mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 07:41:08 +00:00
Merge branch 'master' into tailwind-css
This commit is contained in:
commit
b7e490be9e
63 changed files with 977 additions and 803 deletions
|
@ -1,7 +1,7 @@
|
|||
variables:
|
||||
- &rust_image "rust:1.81"
|
||||
- &install_binstall "wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && cp cargo-binstall /usr/local/cargo/bin"
|
||||
- &install_trunk "cargo-binstall -y trunk@0.21.1"
|
||||
- &install_cargo_leptos "cargo-binstall -y cargo-leptos@0.2.20"
|
||||
|
||||
steps:
|
||||
cargo_fmt:
|
||||
|
@ -50,14 +50,6 @@ steps:
|
|||
- diesel print-schema --config-file=diesel.toml > tmp.schema
|
||||
- diff tmp.schema src/backend/database/schema.rs
|
||||
|
||||
frontend_wasm_build:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- "rustup target add wasm32-unknown-unknown"
|
||||
- "cargo check --target wasm32-unknown-unknown --features csr,hydrate --no-default-features"
|
||||
|
||||
cargo_clippy:
|
||||
image: *rust_image
|
||||
environment:
|
||||
|
@ -89,7 +81,7 @@ steps:
|
|||
commands:
|
||||
- *install_binstall
|
||||
- rustup target add wasm32-unknown-unknown
|
||||
- *install_trunk
|
||||
- *install_cargo_leptos
|
||||
- export PATH="$PATH:$(pwd)/.cargo_home/bin/"
|
||||
- ./scripts/build_release.sh
|
||||
when:
|
||||
|
|
1252
Cargo.lock
generated
1252
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
51
Cargo.toml
51
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ibis"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
|
@ -18,9 +18,32 @@ ssr = [
|
|||
"activitypub_federation",
|
||||
"jsonwebtoken",
|
||||
"katex/duktape",
|
||||
"leptos/ssr",
|
||||
"leptos-use/ssr",
|
||||
]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "katex/wasm-js"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"leptos_meta/hydrate",
|
||||
"leptos_router/hydrate",
|
||||
"katex/wasm-js",
|
||||
]
|
||||
|
||||
# This profile significantly speeds up build time. If debug info is needed you can comment the line
|
||||
# out temporarily, but make sure to leave this in the main branch.
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
|
@ -80,25 +103,23 @@ doku = "0.21.1"
|
|||
smart-default = "0.7.1"
|
||||
tower-layer = "0.3.3"
|
||||
katex = { version = "0.4", default-features = false }
|
||||
include_dir = "0.7.4"
|
||||
markdown-it-block-spoiler = "1.0.0"
|
||||
markdown-it-heading-anchors = "0.3.0"
|
||||
markdown-it-footnote = "0.2.0"
|
||||
markdown-it-sub = "1.0.0"
|
||||
markdown-it-sup = "1.0.0"
|
||||
leptos-use = "0.13.6"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
[package.metadata.leptos]
|
||||
output-name = "ibis"
|
||||
assets-dir = "assets"
|
||||
bin-features = ["ssr"]
|
||||
lib-features = ["csr"]
|
||||
lib-features = ["hydrate"]
|
||||
lib-profile-release = "wasm-release"
|
||||
|
||||
[lib]
|
||||
name = "ibis_lib"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
# This profile significantly speeds up build time. If debug info is needed you can comment the line
|
||||
# out temporarily, but make sure to leave this in the main branch.
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
#opt-level = "z" # Optimize for size.
|
||||
|
|
19
README.md
19
README.md
|
@ -7,6 +7,13 @@ The project uses the same technology as [Lemmy](https://join-lemmy.org/) and ben
|
|||
|
||||
Read the [Project Announcement](https://ibis.wiki/article/Announcing_Ibis,_the_federated_Wikipedia_Alternative) for more information.
|
||||
|
||||
## Community
|
||||
|
||||
Discuss with other Ibis users on Matrix or Lemmy:
|
||||
|
||||
- [Matrix](https://matrix.to/#/#ibis:matrix.org)
|
||||
- [Lemmy](https://lemmy.ml/c/ibis)
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Usage Instructions](https://ibis.wiki/article/Usage_Instructions)
|
||||
|
@ -24,14 +31,22 @@ psql -c "CREATE USER ibis WITH PASSWORD 'ibis' SUPERUSER;" -U postgres
|
|||
psql -c "CREATE DATABASE ibis WITH OWNER ibis;" -U postgres
|
||||
```
|
||||
|
||||
You need to install [cargo](https://rustup.rs/), [trunk](https://trunkrs.dev) and [cargo watch](https://github.com/watchexec/cargo-watch). Run `./scripts/watch.sh` which automatically rebuilds the project after changes. Then open the site at [127.0.0.1:8080](http://127.0.0.1:8080/). Then login with user `ibis` and password `ibis`.
|
||||
You need to install [cargo](https://rustup.rs/), and [cargo-leptos](https://github.com/leptos-rs/cargo-leptos). Run `cargo leptos watch` which automatically rebuilds the project after changes. Then open the site at [localhost:3000](http://localhost:3000/). You can login with user `ibis` and password `ibis`.
|
||||
|
||||
By default the frontend runs on port 8080, which can be changed with env var `TRUNK_SERVE_PORT`. The backend port is 8081 and can be changed with `IBIS_BACKEND_PORT`.
|
||||
By default the frontend runs on port 3000, which can be changed with env var `TRUNK_SERVE_PORT`. The backend port is 8081 and can be changed with `IBIS_BACKEND_PORT`.
|
||||
|
||||
## Federation
|
||||
|
||||
Main objects in terms of federation are the `Instance` and `Article`. Each article belongs to a single origin instance, the one where it was originally created. Articles have a collection of `Edit`s a custom ActivityPub type containing a diff. The text of any article can be built by starting from empty string and applying all associated edits in order. Instances can synchronize their articles with each other, and follow each other to receive updates about articles. Edits are done with diffs which are generated on the backend, and allow for conflict resolution similar to git. Editing also works over federation. In this case an activity `Update/Edit` is sent to the origin instance. If the diff applies cleanly, the origin instance sends the new text in an `Update/Article` activity to its followers. In case there is a conflict, a `Reject` activity is sent back, the editor needs to resolve and resubmit the edit.
|
||||
|
||||
## Donate
|
||||
|
||||
Developing a project like this takes a significant amount of work. You can help funding it with donations:
|
||||
|
||||
- [Liberapay](https://liberapay.com/Ibis/)
|
||||
- [Bitcoin](bitcoin:bc1q6mqlqc84q2h55jkkjvex4kc6h9h534rj87rv2l)
|
||||
- [Monero](monero:84xnACZv82UNTEGNkttLTH8sCeV9Cdr8dHMJSNP6V2hEJW7C17S9xQTUCghwG8TePrRD9wfiPRWcwYvSTHUNoyJ4AXnQYLD)
|
||||
|
||||
## License
|
||||
|
||||
[AGPL](LICENSE)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[build]
|
||||
filehash = false
|
||||
target = "assets/index.html"
|
||||
dist = "assets/dist"
|
||||
minify = "on_release"
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -5,8 +5,12 @@
|
|||
href=".."
|
||||
data-bin="ibis"
|
||||
data-cargo-no-default-features
|
||||
<<<<<<< HEAD
|
||||
data-cargo-features="csr,hydrate" />
|
||||
<link data-trunk rel="tailwind-css" href="/daisyui.css" />
|
||||
=======
|
||||
data-cargo-features="hydrate" />
|
||||
>>>>>>> master
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
85
assets/tailwind.js
Normal file
85
assets/tailwind.js
Normal file
File diff suppressed because one or more lines are too long
20
build.rs
20
build.rs
|
@ -1,20 +0,0 @@
|
|||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::Result,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Create placeholders for wasm files so that `cargo check` etc work without explicitly building
|
||||
/// frontend.
|
||||
fn main() -> Result<()> {
|
||||
create_dir_all("assets/dist/")?;
|
||||
let js = "assets/dist/ibis.js";
|
||||
if !Path::new(js).exists() {
|
||||
File::create(js)?;
|
||||
}
|
||||
let wasm = "assets/dist/ibis_bg.wasm";
|
||||
if !Path::new(wasm).exists() {
|
||||
File::create(wasm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
# Address where ibis should listen for incoming requests
|
||||
bind = "127.0.0.1:8081"
|
||||
|
||||
# Whether users can create new accounts
|
||||
registration_open = true
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
CARGO_TARGET_DIR=target/frontend trunk build --release
|
||||
cargo build --release
|
||||
cargo leptos build --release
|
||||
gzip target/release/ibis -c > ibis.gz
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
IBIS__BIND="${IBIS_BIND:-"127.0.0.1:8081"}"
|
||||
|
||||
killall trunk || true
|
||||
|
||||
# run processes in parallel
|
||||
# https://stackoverflow.com/a/52033580
|
||||
(trap 'kill 0' INT;
|
||||
# start frontend
|
||||
CARGO_TARGET_DIR=target/frontend trunk serve -w src/frontend/ -w assets/ --proxy-backend http://$IBIS__BIND &
|
||||
# start backend, with separate target folder to avoid rebuilds from arch change
|
||||
cargo watch --ignore assets/ibis.css --exec run
|
||||
)
|
|
@ -1,91 +1,61 @@
|
|||
use super::error::MyResult;
|
||||
use anyhow::anyhow;
|
||||
use crate::frontend::app::App;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Path,
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_macros::debug_handler;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::header::HeaderMap;
|
||||
use std::fs::read_to_string;
|
||||
use leptos::LeptosOptions;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub fn asset_routes() -> MyResult<Router<()>> {
|
||||
Ok(Router::new()
|
||||
.route("/assets/ibis.css", get(ibis_css))
|
||||
.route(
|
||||
"/assets/simple.css",
|
||||
get((css_headers(), include_str!("../../assets/simple.css"))),
|
||||
)
|
||||
.route(
|
||||
"/assets/daisyui.css",
|
||||
get((css_headers(), include_str!("../../assets/daisyui.css"))),
|
||||
)
|
||||
.route(
|
||||
"/assets/katex.min.css",
|
||||
get((css_headers(), include_str!("../../assets/katex.min.css"))),
|
||||
)
|
||||
.route("/assets/fonts/*font", get(get_font))
|
||||
.route(
|
||||
"/assets/index.html",
|
||||
get(include_str!("../../assets/index.html")),
|
||||
)
|
||||
.route("/pkg/ibis.js", get(serve_js))
|
||||
.route("/pkg/ibis_bg.wasm", get(serve_wasm)))
|
||||
}
|
||||
// from https://github.com/leptos-rs/start-axum
|
||||
|
||||
fn css_headers() -> HeaderMap {
|
||||
static INSTANCE: OnceCell<HeaderMap> = OnceCell::new();
|
||||
INSTANCE
|
||||
.get_or_init(|| {
|
||||
let mut css_headers = HeaderMap::new();
|
||||
let val = "text/css".parse().expect("valid header value");
|
||||
css_headers.insert("Content-Type", val);
|
||||
css_headers
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
#[debug_handler]
|
||||
pub async fn file_and_error_handler(
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let root = options.site_root.clone();
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
async fn ibis_css() -> MyResult<(HeaderMap, Response<Body>)> {
|
||||
let res = if cfg!(debug_assertions) {
|
||||
read_to_string("assets/ibis.css")?.into_response()
|
||||
let mut static_parts = parts.clone();
|
||||
static_parts.headers.clear();
|
||||
if let Some(encodings) = parts.headers.get("accept-encoding") {
|
||||
static_parts
|
||||
.headers
|
||||
.insert("accept-encoding", encodings.clone());
|
||||
}
|
||||
|
||||
let res = get_static_file(Request::from_parts(static_parts, Body::empty()), &root).await?;
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
Ok(res.into_response())
|
||||
} else {
|
||||
include_str!("../../assets/ibis.css").into_response()
|
||||
};
|
||||
Ok((css_headers(), res))
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), App);
|
||||
Ok(handler(Request::from_parts(parts, body))
|
||||
.await
|
||||
.into_response())
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn serve_js() -> MyResult<impl IntoResponse> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/javascript".parse()?);
|
||||
let content = include_str!("../../assets/dist/ibis.js");
|
||||
Ok((headers, content))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn serve_wasm() -> MyResult<impl IntoResponse> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/wasm".parse()?);
|
||||
let content = include_bytes!("../../assets/dist/ibis_bg.wasm");
|
||||
Ok((headers, content))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
async fn get_font(Path(font): Path<String>) -> MyResult<impl IntoResponse> {
|
||||
let mut headers = HeaderMap::new();
|
||||
let content_type = if font.ends_with(".ttf") {
|
||||
"font/ttf"
|
||||
} else if font.ends_with(".woff") {
|
||||
"font/woff"
|
||||
} else if font.ends_with(".woff2") {
|
||||
"font/woff2"
|
||||
} else {
|
||||
return Err(anyhow!("invalid font").into());
|
||||
};
|
||||
headers.insert("Content-type", content_type.parse()?);
|
||||
let content = std::fs::read("assets/fonts/".to_owned() + &font)?;
|
||||
Ok((headers, content))
|
||||
async fn get_static_file(
|
||||
request: Request<Body>,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root)
|
||||
.precompressed_gzip()
|
||||
.precompressed_br()
|
||||
.oneshot(request)
|
||||
.await
|
||||
{
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error serving files: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,11 @@ use config::Config;
|
|||
use doku::Document;
|
||||
use serde::Deserialize;
|
||||
use smart_default::SmartDefault;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
|
||||
#[serde(default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IbisConfig {
|
||||
/// Address where ibis should listen for incoming requests
|
||||
#[default("127.0.0.1:8081".parse().expect("parse config bind"))]
|
||||
#[doku(as = "String", example = "127.0.0.1:8081")]
|
||||
pub bind: SocketAddr,
|
||||
/// Details about the PostgreSQL database connection
|
||||
pub database: IbisConfigDatabase,
|
||||
/// Whether users can create new accounts
|
||||
|
@ -37,6 +33,7 @@ impl IbisConfig {
|
|||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
|
||||
#[serde(default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IbisConfigDatabase {
|
||||
/// Database connection url
|
||||
#[default("postgres://ibis:password@localhost:5432/ibis")]
|
||||
|
@ -50,6 +47,7 @@ pub struct IbisConfigDatabase {
|
|||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
|
||||
#[serde(default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IbisConfigSetup {
|
||||
#[default("ibis")]
|
||||
#[doku(example = "ibis")]
|
||||
|
@ -61,6 +59,7 @@ pub struct IbisConfigSetup {
|
|||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
|
||||
#[serde(default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IbisConfigFederation {
|
||||
/// Domain name of the instance, mandatory for federation
|
||||
#[default("example.com")]
|
||||
|
|
|
@ -22,7 +22,7 @@ use activitypub_federation::{
|
|||
http_signatures::generate_actor_keypair,
|
||||
};
|
||||
use api::api_routes;
|
||||
use assets::asset_routes;
|
||||
use assets::file_and_error_handler;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{HeaderValue, Request},
|
||||
|
@ -38,9 +38,10 @@ use diesel::{
|
|||
PgConnection,
|
||||
};
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use leptos::leptos_config::get_config_from_str;
|
||||
use leptos::get_configuration;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use log::info;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_layer::Layer;
|
||||
|
@ -58,7 +59,7 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
|||
|
||||
const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes";
|
||||
|
||||
pub async fn start(config: IbisConfig) -> MyResult<()> {
|
||||
pub async fn start(config: IbisConfig, override_hostname: Option<SocketAddr>) -> MyResult<()> {
|
||||
let manager = ConnectionManager::<PgConnection>::new(&config.database.connection_url);
|
||||
let db_pool = Pool::builder()
|
||||
.max_size(config.database.pool_size)
|
||||
|
@ -82,15 +83,18 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
|
|||
setup(&data.to_request_data()).await?;
|
||||
}
|
||||
|
||||
let mut conf = get_config_from_str(include_str!("../../Cargo.toml"))?;
|
||||
conf.site_addr = data.config.bind;
|
||||
let leptos_options = get_configuration(Some("Cargo.toml")).await?.leptos_options;
|
||||
let mut addr = leptos_options.site_addr;
|
||||
if let Some(override_hostname) = override_hostname {
|
||||
addr = override_hostname;
|
||||
}
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let config = data.clone();
|
||||
let app = Router::new()
|
||||
.leptos_routes(&conf, routes, App)
|
||||
.with_state(conf)
|
||||
.nest("", asset_routes()?)
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options)
|
||||
.nest(FEDERATION_ROUTES_PREFIX, federation_routes())
|
||||
.nest("/api/v1", api_routes())
|
||||
.nest("", nodeinfo::config())
|
||||
|
@ -102,8 +106,8 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
|
|||
let middleware = axum::middleware::from_fn(federation_routes_middleware);
|
||||
let app_with_middleware = middleware.layer(app);
|
||||
|
||||
info!("Listening on {}", &data.config.bind);
|
||||
let listener = TcpListener::bind(&data.config.bind).await?;
|
||||
info!("Listening on {}", &addr);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, app_with_middleware.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -47,10 +47,9 @@ impl ApiClient {
|
|||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
hostname = crate::backend::config::IbisConfig::read()
|
||||
.unwrap()
|
||||
.bind
|
||||
.to_string();
|
||||
use leptos::leptos_config::get_config_from_str;
|
||||
let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap();
|
||||
hostname = leptos_options.site_addr.to_string();
|
||||
ssl = false;
|
||||
}
|
||||
// required for tests
|
||||
|
|
|
@ -91,10 +91,10 @@ pub fn App() -> impl IntoView {
|
|||
|
||||
view! {
|
||||
<>
|
||||
<Stylesheet id="daisyui" href="/assets/daisyui.css" />
|
||||
<Stylesheet id="ibis" href="/assets/ibis.css" />
|
||||
<Stylesheet id="katex" href="/assets/katex.min.css" />
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries"></script>
|
||||
<Stylesheet id="daisyui" href="/daisyui.css" />
|
||||
<Stylesheet id="ibis" href="/ibis.css" />
|
||||
<Stylesheet id="katex" href="/katex.min.css" />
|
||||
<script src="/tailwind.js"></script>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
|
|
|
@ -29,10 +29,14 @@ pub fn render_markdown(text: &str) -> String {
|
|||
fn markdown_parser() -> MarkdownIt {
|
||||
let mut parser = MarkdownIt::new();
|
||||
markdown_it::plugins::cmark::add(&mut parser);
|
||||
markdown_it::plugins::extra::linkify::add(&mut parser);
|
||||
markdown_it_heading_anchors::add(&mut parser);
|
||||
markdown_it_footnote::add(&mut parser);
|
||||
markdown_it::plugins::extra::strikethrough::add(&mut parser);
|
||||
markdown_it::plugins::extra::tables::add(&mut parser);
|
||||
markdown_it::plugins::extra::typographer::add(&mut parser);
|
||||
markdown_it_block_spoiler::add(&mut parser);
|
||||
markdown_it_sub::add(&mut parser);
|
||||
markdown_it_sup::add(&mut parser);
|
||||
parser.inline.add_rule::<ArticleLinkScanner>();
|
||||
parser.inline.add_rule::<MathEquationScanner>();
|
||||
parser
|
||||
|
@ -115,7 +119,7 @@ impl InlineRule for MathEquationScanner {
|
|||
return None;
|
||||
}
|
||||
let mut display_mode = false;
|
||||
if input.starts_with("$$\n") {
|
||||
if input.starts_with("$$\n") || input.starts_with("$$ ") {
|
||||
display_mode = true;
|
||||
}
|
||||
const SEPARATOR_LENGTH: usize = 2;
|
||||
|
|
|
@ -11,7 +11,17 @@ pub mod pages;
|
|||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {}
|
||||
pub fn hydrate() {
|
||||
use crate::frontend::app::App;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
|
||||
// set theme
|
||||
// https://daisyui.com/docs/themes/
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let html_element = document.document_element().unwrap();
|
||||
html_element.set_attribute("data-theme", "emerald").unwrap();
|
||||
}
|
||||
|
||||
fn article_link(article: &DbArticle) -> String {
|
||||
if article.local {
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
use crate::{common::CreateArticleForm, frontend::app::GlobalState};
|
||||
use crate::{
|
||||
common::CreateArticleForm,
|
||||
frontend::{app::GlobalState, markdown::render_markdown},
|
||||
};
|
||||
use html::Textarea;
|
||||
use leptos::*;
|
||||
use leptos_router::Redirect;
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
||||
#[component]
|
||||
pub fn CreateArticle() -> impl IntoView {
|
||||
let (title, set_title) = create_signal(String::new());
|
||||
let (text, set_text) = create_signal(String::new());
|
||||
let textarea = create_node_ref::<Textarea>();
|
||||
let UseTextareaAutosizeReturn {
|
||||
content,
|
||||
set_content,
|
||||
trigger_resize: _,
|
||||
} = use_textarea_autosize(textarea);
|
||||
let (summary, set_summary) = create_signal(String::new());
|
||||
let (show_preview, set_show_preview) = create_signal(false);
|
||||
let (preview, set_preview) = create_signal(String::new());
|
||||
let (create_response, set_create_response) = create_signal(None::<()>);
|
||||
let (create_error, set_create_error) = create_signal(None::<String>);
|
||||
let (wait_for_response, set_wait_for_response) = create_signal(false);
|
||||
|
@ -58,12 +70,21 @@ pub fn CreateArticle() -> impl IntoView {
|
|||
/>
|
||||
|
||||
<textarea
|
||||
value=content
|
||||
placeholder="Article text..."
|
||||
on:keyup=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_text.update(|p| *p = val);
|
||||
on:input=move |evt| {
|
||||
let val = event_target_value(&evt);
|
||||
set_preview.set(render_markdown(&val));
|
||||
set_content.set(val);
|
||||
}
|
||||
node_ref=textarea
|
||||
></textarea>
|
||||
<button on:click=move |_| {
|
||||
set_show_preview.update(|s| *s = !*s)
|
||||
}>Preview</button>
|
||||
<Show when=move || { show_preview.get() }>
|
||||
<div id="preview" inner_html=move || preview.get()></div>
|
||||
</Show>
|
||||
<div>
|
||||
<a href="https://commonmark.org/help/" target="blank_">
|
||||
Markdown
|
||||
|
@ -90,7 +111,7 @@ pub fn CreateArticle() -> impl IntoView {
|
|||
<button
|
||||
prop:disabled=move || button_is_disabled.get()
|
||||
on:click=move |_| {
|
||||
submit_action.dispatch((title.get(), text.get(), summary.get()))
|
||||
submit_action.dispatch((title.get(), content.get(), summary.get()))
|
||||
}
|
||||
>
|
||||
Submit
|
||||
|
|
|
@ -7,8 +7,10 @@ use crate::{
|
|||
pages::article_resource,
|
||||
},
|
||||
};
|
||||
use html::Textarea;
|
||||
use leptos::*;
|
||||
use leptos_router::use_params_map;
|
||||
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum EditResponse {
|
||||
|
@ -44,7 +46,12 @@ pub fn EditArticle() -> impl IntoView {
|
|||
.dispatch(conflict_id);
|
||||
}
|
||||
|
||||
let (text, set_text) = create_signal(String::new());
|
||||
let textarea = create_node_ref::<Textarea>();
|
||||
let UseTextareaAutosizeReturn {
|
||||
content,
|
||||
set_content,
|
||||
trigger_resize: _,
|
||||
} = use_textarea_autosize(textarea);
|
||||
let (summary, set_summary) = create_signal(String::new());
|
||||
let (show_preview, set_show_preview) = create_signal(false);
|
||||
let (preview, set_preview) = create_signal(String::new());
|
||||
|
@ -118,10 +125,9 @@ pub fn EditArticle() -> impl IntoView {
|
|||
article.article.text = conflict.three_way_merge;
|
||||
set_summary.set(conflict.summary);
|
||||
}
|
||||
set_text.set(article.article.text.clone());
|
||||
set_content.set(article.article.text.clone());
|
||||
set_preview.set(render_markdown(&article.article.text));
|
||||
let article_ = article.clone();
|
||||
let rows = article.article.text.lines().count() + 1;
|
||||
view! {
|
||||
// set initial text, otherwise submit with no changes results in empty text
|
||||
<div>
|
||||
|
@ -133,14 +139,15 @@ pub fn EditArticle() -> impl IntoView {
|
|||
})
|
||||
}}
|
||||
<textarea
|
||||
value=content
|
||||
id="edit-article-textarea"
|
||||
class="textarea textarea-bordered textarea-primary min-w-full"
|
||||
rows=rows
|
||||
on:keyup=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
on:input=move |evt| {
|
||||
let val = event_target_value(&evt);
|
||||
set_preview.set(render_markdown(&val));
|
||||
set_text.set(val);
|
||||
set_content.set(val);
|
||||
}
|
||||
node_ref=textarea
|
||||
>
|
||||
{article.article.text.clone()}
|
||||
</textarea>
|
||||
|
@ -174,7 +181,7 @@ pub fn EditArticle() -> impl IntoView {
|
|||
on:click=move |_| {
|
||||
submit_action
|
||||
.dispatch((
|
||||
text.get(),
|
||||
content.get(),
|
||||
summary.get(),
|
||||
article_.clone(),
|
||||
edit_response.get(),
|
||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -1,7 +1,7 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
|
||||
use ibis_lib::backend::config::IbisConfig;
|
||||
pub async fn main() -> ibis::backend::error::MyResult<()> {
|
||||
use ibis::backend::config::IbisConfig;
|
||||
use log::LevelFilter;
|
||||
|
||||
if std::env::args().collect::<Vec<_>>().get(1) == Some(&"--print-config".to_string()) {
|
||||
|
@ -16,24 +16,6 @@ pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
|
|||
.init();
|
||||
|
||||
let ibis_config = IbisConfig::read()?;
|
||||
ibis_lib::backend::start(ibis_config).await?;
|
||||
ibis::backend::start(ibis_config, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn main() {
|
||||
use ibis_lib::frontend::app::App;
|
||||
use leptos::{mount_to_body, view};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! { <App /> }
|
||||
});
|
||||
|
||||
// set theme
|
||||
// https://daisyui.com/docs/themes/
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let html_element = document.document_element().unwrap();
|
||||
html_element.set_attribute("data-theme", "emerald").unwrap();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use ibis_lib::{
|
||||
use ibis::{
|
||||
backend::{
|
||||
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
||||
start,
|
||||
|
@ -21,7 +21,7 @@ use std::{
|
|||
thread::{sleep, spawn},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{join, task::JoinHandle};
|
||||
use tracing::log::LevelFilter;
|
||||
|
||||
pub struct TestData {
|
||||
|
@ -66,11 +66,13 @@ impl TestData {
|
|||
j.join().unwrap();
|
||||
}
|
||||
|
||||
Self {
|
||||
alpha: IbisInstance::start(alpha_db_path, port_alpha, "alpha").await,
|
||||
beta: IbisInstance::start(beta_db_path, port_beta, "beta").await,
|
||||
gamma: IbisInstance::start(gamma_db_path, port_gamma, "gamma").await,
|
||||
}
|
||||
let (alpha, beta, gamma) = join!(
|
||||
IbisInstance::start(alpha_db_path, port_alpha, "alpha"),
|
||||
IbisInstance::start(beta_db_path, port_beta, "beta"),
|
||||
IbisInstance::start(gamma_db_path, port_gamma, "gamma")
|
||||
);
|
||||
|
||||
Self { alpha, beta, gamma }
|
||||
}
|
||||
|
||||
pub fn stop(self) -> MyResult<()> {
|
||||
|
@ -115,23 +117,26 @@ impl IbisInstance {
|
|||
|
||||
async fn start(db_path: String, port: i32, username: &str) -> Self {
|
||||
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
|
||||
let hostname = format!("localhost:{port}");
|
||||
let bind = format!("127.0.0.1:{port}").parse().unwrap();
|
||||
let hostname = format!("127.0.0.1:{port}");
|
||||
let domain = format!("localhost:{port}");
|
||||
let config = IbisConfig {
|
||||
bind,
|
||||
database: IbisConfigDatabase {
|
||||
connection_url,
|
||||
..Default::default()
|
||||
},
|
||||
registration_open: true,
|
||||
federation: IbisConfigFederation {
|
||||
domain: hostname.clone(),
|
||||
domain: domain.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
|
||||
let api_client = ApiClient::new(client, Some(domain));
|
||||
let handle = tokio::task::spawn(async move {
|
||||
start(config).await.unwrap();
|
||||
start(config, Some(hostname.parse().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
// wait a moment for the backend to start
|
||||
tokio::time::sleep(Duration::from_millis(5000)).await;
|
||||
|
@ -139,8 +144,6 @@ impl IbisInstance {
|
|||
username: username.to_string(),
|
||||
password: "hunter2".to_string(),
|
||||
};
|
||||
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
|
||||
let api_client = ApiClient::new(client, Some(hostname));
|
||||
api_client.register(form).await.unwrap();
|
||||
Self {
|
||||
api_client,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
extern crate ibis_lib;
|
||||
|
||||
mod common;
|
||||
|
||||
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
|
||||
use ibis_lib::{
|
||||
use ibis::{
|
||||
common::{
|
||||
utils::extract_domain,
|
||||
ArticleView,
|
||||
|
|
Loading…
Reference in a new issue