1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-11-22 13:31:08 +00:00

Merge branch 'master' into tailwind-css

This commit is contained in:
Felix Ableitner 2024-10-29 15:21:02 +01:00
commit b7e490be9e
63 changed files with 977 additions and 803 deletions

View file

@ -1,7 +1,7 @@
variables: variables:
- &rust_image "rust:1.81" - &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_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: steps:
cargo_fmt: cargo_fmt:
@ -50,14 +50,6 @@ steps:
- diesel print-schema --config-file=diesel.toml > tmp.schema - diesel print-schema --config-file=diesel.toml > tmp.schema
- diff tmp.schema src/backend/database/schema.rs - 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: cargo_clippy:
image: *rust_image image: *rust_image
environment: environment:
@ -89,7 +81,7 @@ steps:
commands: commands:
- *install_binstall - *install_binstall
- rustup target add wasm32-unknown-unknown - rustup target add wasm32-unknown-unknown
- *install_trunk - *install_cargo_leptos
- export PATH="$PATH:$(pwd)/.cargo_home/bin/" - export PATH="$PATH:$(pwd)/.cargo_home/bin/"
- ./scripts/build_release.sh - ./scripts/build_release.sh
when: when:

1252
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ibis" name = "ibis"
version = "0.1.3" version = "0.1.4"
edition = "2021" edition = "2021"
[features] [features]
@ -18,9 +18,32 @@ ssr = [
"activitypub_federation", "activitypub_federation",
"jsonwebtoken", "jsonwebtoken",
"katex/duktape", "katex/duktape",
"leptos/ssr",
"leptos-use/ssr",
] ]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "katex/wasm-js"] hydrate = [
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/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] [lints.clippy]
dbg_macro = "deny" dbg_macro = "deny"
@ -80,25 +103,23 @@ doku = "0.21.1"
smart-default = "0.7.1" smart-default = "0.7.1"
tower-layer = "0.3.3" tower-layer = "0.3.3"
katex = { version = "0.4", default-features = false } 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] [dev-dependencies]
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
[package.metadata.leptos] [package.metadata.leptos]
output-name = "ibis" output-name = "ibis"
assets-dir = "assets"
bin-features = ["ssr"] bin-features = ["ssr"]
lib-features = ["csr"] lib-features = ["hydrate"]
lib-profile-release = "wasm-release"
[lib] [lib]
name = "ibis_lib"
crate-type = ["cdylib", "rlib"] 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.

View file

@ -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. 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 ## Useful links
- [Usage Instructions](https://ibis.wiki/article/Usage_Instructions) - [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 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 ## 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. 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 ## License
[AGPL](LICENSE) [AGPL](LICENSE)

View file

@ -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.

View file

@ -5,8 +5,12 @@
href=".." href=".."
data-bin="ibis" data-bin="ibis"
data-cargo-no-default-features data-cargo-no-default-features
<<<<<<< HEAD
data-cargo-features="csr,hydrate" /> data-cargo-features="csr,hydrate" />
<link data-trunk rel="tailwind-css" href="/daisyui.css" /> <link data-trunk rel="tailwind-css" href="/daisyui.css" />
=======
data-cargo-features="hydrate" />
>>>>>>> master
</head> </head>
<body></body> <body></body>
</html> </html>

85
assets/tailwind.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -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(())
}

View file

@ -1,6 +1,3 @@
# Address where ibis should listen for incoming requests
bind = "127.0.0.1:8081"
# Whether users can create new accounts # Whether users can create new accounts
registration_open = true registration_open = true

View file

@ -1,6 +1,5 @@
#!/bin/sh #!/bin/sh
set -e set -e
CARGO_TARGET_DIR=target/frontend trunk build --release cargo leptos build --release
cargo build --release
gzip target/release/ibis -c > ibis.gz gzip target/release/ibis -c > ibis.gz

View file

@ -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
)

View file

@ -1,91 +1,61 @@
use super::error::MyResult; use crate::frontend::app::App;
use anyhow::anyhow;
use axum::{ use axum::{
body::Body, body::Body,
extract::Path, extract::{Request, State},
http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::get,
Router,
}; };
use axum_macros::debug_handler; use axum_macros::debug_handler;
use once_cell::sync::OnceCell; use leptos::LeptosOptions;
use reqwest::header::HeaderMap; use tower::ServiceExt;
use std::fs::read_to_string; use tower_http::services::ServeDir;
pub fn asset_routes() -> MyResult<Router<()>> { // from https://github.com/leptos-rs/start-axum
Ok(Router::new()
.route("/assets/ibis.css", get(ibis_css)) #[debug_handler]
.route( pub async fn file_and_error_handler(
"/assets/simple.css", State(options): State<LeptosOptions>,
get((css_headers(), include_str!("../../assets/simple.css"))), req: Request<Body>,
) ) -> Result<Response<Body>, (StatusCode, String)> {
.route( let root = options.site_root.clone();
"/assets/daisyui.css", let (parts, body) = req.into_parts();
get((css_headers(), include_str!("../../assets/daisyui.css"))),
) let mut static_parts = parts.clone();
.route( static_parts.headers.clear();
"/assets/katex.min.css", if let Some(encodings) = parts.headers.get("accept-encoding") {
get((css_headers(), include_str!("../../assets/katex.min.css"))), static_parts
) .headers
.route("/assets/fonts/*font", get(get_font)) .insert("accept-encoding", encodings.clone());
.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)))
} }
fn css_headers() -> HeaderMap { let res = get_static_file(Request::from_parts(static_parts, Body::empty()), &root).await?;
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()
}
async fn ibis_css() -> MyResult<(HeaderMap, Response<Body>)> { if res.status() == StatusCode::OK {
let res = if cfg!(debug_assertions) { Ok(res.into_response())
read_to_string("assets/ibis.css")?.into_response()
} else { } else {
include_str!("../../assets/ibis.css").into_response() let handler = leptos_axum::render_app_to_stream(options.to_owned(), App);
}; Ok(handler(Request::from_parts(parts, body))
Ok((css_headers(), res)) .await
.into_response())
}
} }
#[debug_handler] async fn get_static_file(
async fn serve_js() -> MyResult<impl IntoResponse> { request: Request<Body>,
let mut headers = HeaderMap::new(); root: &str,
headers.insert("Content-Type", "application/javascript".parse()?); ) -> Result<Response<Body>, (StatusCode, String)> {
let content = include_str!("../../assets/dist/ibis.js"); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
Ok((headers, content)) // 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}"),
)),
} }
#[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))
} }

View file

@ -3,15 +3,11 @@ use config::Config;
use doku::Document; use doku::Document;
use serde::Deserialize; use serde::Deserialize;
use smart_default::SmartDefault; use smart_default::SmartDefault;
use std::net::SocketAddr;
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
#[serde(default)] #[serde(default)]
#[serde(deny_unknown_fields)]
pub struct IbisConfig { 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 /// Details about the PostgreSQL database connection
pub database: IbisConfigDatabase, pub database: IbisConfigDatabase,
/// Whether users can create new accounts /// Whether users can create new accounts
@ -37,6 +33,7 @@ impl IbisConfig {
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
#[serde(default)] #[serde(default)]
#[serde(deny_unknown_fields)]
pub struct IbisConfigDatabase { pub struct IbisConfigDatabase {
/// Database connection url /// Database connection url
#[default("postgres://ibis:password@localhost:5432/ibis")] #[default("postgres://ibis:password@localhost:5432/ibis")]
@ -50,6 +47,7 @@ pub struct IbisConfigDatabase {
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
#[serde(default)] #[serde(default)]
#[serde(deny_unknown_fields)]
pub struct IbisConfigSetup { pub struct IbisConfigSetup {
#[default("ibis")] #[default("ibis")]
#[doku(example = "ibis")] #[doku(example = "ibis")]
@ -61,6 +59,7 @@ pub struct IbisConfigSetup {
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
#[serde(default)] #[serde(default)]
#[serde(deny_unknown_fields)]
pub struct IbisConfigFederation { pub struct IbisConfigFederation {
/// Domain name of the instance, mandatory for federation /// Domain name of the instance, mandatory for federation
#[default("example.com")] #[default("example.com")]

View file

@ -22,7 +22,7 @@ use activitypub_federation::{
http_signatures::generate_actor_keypair, http_signatures::generate_actor_keypair,
}; };
use api::api_routes; use api::api_routes;
use assets::asset_routes; use assets::file_and_error_handler;
use axum::{ use axum::{
body::Body, body::Body,
http::{HeaderValue, Request}, http::{HeaderValue, Request},
@ -38,9 +38,10 @@ use diesel::{
PgConnection, PgConnection,
}; };
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; 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 leptos_axum::{generate_route_list, LeptosRoutes};
use log::info; use log::info;
use std::net::SocketAddr;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tower_layer::Layer; use tower_layer::Layer;
@ -58,7 +59,7 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes"; 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 manager = ConnectionManager::<PgConnection>::new(&config.database.connection_url);
let db_pool = Pool::builder() let db_pool = Pool::builder()
.max_size(config.database.pool_size) .max_size(config.database.pool_size)
@ -82,15 +83,18 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
setup(&data.to_request_data()).await?; setup(&data.to_request_data()).await?;
} }
let mut conf = get_config_from_str(include_str!("../../Cargo.toml"))?; let leptos_options = get_configuration(Some("Cargo.toml")).await?.leptos_options;
conf.site_addr = data.config.bind; let mut addr = leptos_options.site_addr;
if let Some(override_hostname) = override_hostname {
addr = override_hostname;
}
let routes = generate_route_list(App); let routes = generate_route_list(App);
let config = data.clone(); let config = data.clone();
let app = Router::new() let app = Router::new()
.leptos_routes(&conf, routes, App) .leptos_routes(&leptos_options, routes, App)
.with_state(conf) .fallback(file_and_error_handler)
.nest("", asset_routes()?) .with_state(leptos_options)
.nest(FEDERATION_ROUTES_PREFIX, federation_routes()) .nest(FEDERATION_ROUTES_PREFIX, federation_routes())
.nest("/api/v1", api_routes()) .nest("/api/v1", api_routes())
.nest("", nodeinfo::config()) .nest("", nodeinfo::config())
@ -102,8 +106,8 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
let middleware = axum::middleware::from_fn(federation_routes_middleware); let middleware = axum::middleware::from_fn(federation_routes_middleware);
let app_with_middleware = middleware.layer(app); let app_with_middleware = middleware.layer(app);
info!("Listening on {}", &data.config.bind); info!("Listening on {}", &addr);
let listener = TcpListener::bind(&data.config.bind).await?; let listener = TcpListener::bind(&addr).await?;
axum::serve(listener, app_with_middleware.into_make_service()).await?; axum::serve(listener, app_with_middleware.into_make_service()).await?;
Ok(()) Ok(())

View file

@ -47,10 +47,9 @@ impl ApiClient {
} }
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
{ {
hostname = crate::backend::config::IbisConfig::read() use leptos::leptos_config::get_config_from_str;
.unwrap() let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap();
.bind hostname = leptos_options.site_addr.to_string();
.to_string();
ssl = false; ssl = false;
} }
// required for tests // required for tests

View file

@ -91,10 +91,10 @@ pub fn App() -> impl IntoView {
view! { view! {
<> <>
<Stylesheet id="daisyui" href="/assets/daisyui.css" /> <Stylesheet id="daisyui" href="/daisyui.css" />
<Stylesheet id="ibis" href="/assets/ibis.css" /> <Stylesheet id="ibis" href="/ibis.css" />
<Stylesheet id="katex" href="/assets/katex.min.css" /> <Stylesheet id="katex" href="/katex.min.css" />
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries"></script> <script src="/tailwind.js"></script>
<Router> <Router>
<Nav /> <Nav />
<main> <main>

View file

@ -29,10 +29,14 @@ pub fn render_markdown(text: &str) -> String {
fn markdown_parser() -> MarkdownIt { fn markdown_parser() -> MarkdownIt {
let mut parser = MarkdownIt::new(); let mut parser = MarkdownIt::new();
markdown_it::plugins::cmark::add(&mut parser); 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::strikethrough::add(&mut parser);
markdown_it::plugins::extra::tables::add(&mut parser); markdown_it::plugins::extra::tables::add(&mut parser);
markdown_it::plugins::extra::typographer::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::<ArticleLinkScanner>();
parser.inline.add_rule::<MathEquationScanner>(); parser.inline.add_rule::<MathEquationScanner>();
parser parser
@ -115,7 +119,7 @@ impl InlineRule for MathEquationScanner {
return None; return None;
} }
let mut display_mode = false; let mut display_mode = false;
if input.starts_with("$$\n") { if input.starts_with("$$\n") || input.starts_with("$$ ") {
display_mode = true; display_mode = true;
} }
const SEPARATOR_LENGTH: usize = 2; const SEPARATOR_LENGTH: usize = 2;

View file

@ -11,7 +11,17 @@ pub mod pages;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen] #[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 { fn article_link(article: &DbArticle) -> String {
if article.local { if article.local {

View file

@ -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::*;
use leptos_router::Redirect; use leptos_router::Redirect;
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
#[component] #[component]
pub fn CreateArticle() -> impl IntoView { pub fn CreateArticle() -> impl IntoView {
let (title, set_title) = create_signal(String::new()); 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 (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_response, set_create_response) = create_signal(None::<()>);
let (create_error, set_create_error) = create_signal(None::<String>); let (create_error, set_create_error) = create_signal(None::<String>);
let (wait_for_response, set_wait_for_response) = create_signal(false); let (wait_for_response, set_wait_for_response) = create_signal(false);
@ -58,12 +70,21 @@ pub fn CreateArticle() -> impl IntoView {
/> />
<textarea <textarea
value=content
placeholder="Article text..." placeholder="Article text..."
on:keyup=move |ev| { on:input=move |evt| {
let val = event_target_value(&ev); let val = event_target_value(&evt);
set_text.update(|p| *p = val); set_preview.set(render_markdown(&val));
set_content.set(val);
} }
node_ref=textarea
></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> <div>
<a href="https://commonmark.org/help/" target="blank_"> <a href="https://commonmark.org/help/" target="blank_">
Markdown Markdown
@ -90,7 +111,7 @@ pub fn CreateArticle() -> impl IntoView {
<button <button
prop:disabled=move || button_is_disabled.get() prop:disabled=move || button_is_disabled.get()
on:click=move |_| { on:click=move |_| {
submit_action.dispatch((title.get(), text.get(), summary.get())) submit_action.dispatch((title.get(), content.get(), summary.get()))
} }
> >
Submit Submit

View file

@ -7,8 +7,10 @@ use crate::{
pages::article_resource, pages::article_resource,
}, },
}; };
use html::Textarea;
use leptos::*; use leptos::*;
use leptos_router::use_params_map; use leptos_router::use_params_map;
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
enum EditResponse { enum EditResponse {
@ -44,7 +46,12 @@ pub fn EditArticle() -> impl IntoView {
.dispatch(conflict_id); .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 (summary, set_summary) = create_signal(String::new());
let (show_preview, set_show_preview) = create_signal(false); let (show_preview, set_show_preview) = create_signal(false);
let (preview, set_preview) = create_signal(String::new()); let (preview, set_preview) = create_signal(String::new());
@ -118,10 +125,9 @@ pub fn EditArticle() -> impl IntoView {
article.article.text = conflict.three_way_merge; article.article.text = conflict.three_way_merge;
set_summary.set(conflict.summary); 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)); set_preview.set(render_markdown(&article.article.text));
let article_ = article.clone(); let article_ = article.clone();
let rows = article.article.text.lines().count() + 1;
view! { view! {
// set initial text, otherwise submit with no changes results in empty text // set initial text, otherwise submit with no changes results in empty text
<div> <div>
@ -133,14 +139,15 @@ pub fn EditArticle() -> impl IntoView {
}) })
}} }}
<textarea <textarea
value=content
id="edit-article-textarea" id="edit-article-textarea"
class="textarea textarea-bordered textarea-primary min-w-full" class="textarea textarea-bordered textarea-primary min-w-full"
rows=rows on:input=move |evt| {
on:keyup=move |ev| { let val = event_target_value(&evt);
let val = event_target_value(&ev);
set_preview.set(render_markdown(&val)); set_preview.set(render_markdown(&val));
set_text.set(val); set_content.set(val);
} }
node_ref=textarea
> >
{article.article.text.clone()} {article.article.text.clone()}
</textarea> </textarea>
@ -174,7 +181,7 @@ pub fn EditArticle() -> impl IntoView {
on:click=move |_| { on:click=move |_| {
submit_action submit_action
.dispatch(( .dispatch((
text.get(), content.get(),
summary.get(), summary.get(),
article_.clone(), article_.clone(),
edit_response.get(), edit_response.get(),

View file

@ -1,7 +1,7 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
pub async fn main() -> ibis_lib::backend::error::MyResult<()> { pub async fn main() -> ibis::backend::error::MyResult<()> {
use ibis_lib::backend::config::IbisConfig; use ibis::backend::config::IbisConfig;
use log::LevelFilter; use log::LevelFilter;
if std::env::args().collect::<Vec<_>>().get(1) == Some(&"--print-config".to_string()) { 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(); .init();
let ibis_config = IbisConfig::read()?; let ibis_config = IbisConfig::read()?;
ibis_lib::backend::start(ibis_config).await?; ibis::backend::start(ibis_config, None).await?;
Ok(()) 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();
}

View file

@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use ibis_lib::{ use ibis::{
backend::{ backend::{
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation}, config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
start, start,
@ -21,7 +21,7 @@ use std::{
thread::{sleep, spawn}, thread::{sleep, spawn},
time::Duration, time::Duration,
}; };
use tokio::task::JoinHandle; use tokio::{join, task::JoinHandle};
use tracing::log::LevelFilter; use tracing::log::LevelFilter;
pub struct TestData { pub struct TestData {
@ -66,11 +66,13 @@ impl TestData {
j.join().unwrap(); j.join().unwrap();
} }
Self { let (alpha, beta, gamma) = join!(
alpha: IbisInstance::start(alpha_db_path, port_alpha, "alpha").await, IbisInstance::start(alpha_db_path, port_alpha, "alpha"),
beta: IbisInstance::start(beta_db_path, port_beta, "beta").await, IbisInstance::start(beta_db_path, port_beta, "beta"),
gamma: IbisInstance::start(gamma_db_path, port_gamma, "gamma").await, IbisInstance::start(gamma_db_path, port_gamma, "gamma")
} );
Self { alpha, beta, gamma }
} }
pub fn stop(self) -> MyResult<()> { pub fn stop(self) -> MyResult<()> {
@ -115,23 +117,26 @@ impl IbisInstance {
async fn start(db_path: String, port: i32, username: &str) -> Self { async fn start(db_path: String, port: i32, username: &str) -> Self {
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}"); let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
let hostname = format!("localhost:{port}"); let hostname = format!("127.0.0.1:{port}");
let bind = format!("127.0.0.1:{port}").parse().unwrap(); let domain = format!("localhost:{port}");
let config = IbisConfig { let config = IbisConfig {
bind,
database: IbisConfigDatabase { database: IbisConfigDatabase {
connection_url, connection_url,
..Default::default() ..Default::default()
}, },
registration_open: true, registration_open: true,
federation: IbisConfigFederation { federation: IbisConfigFederation {
domain: hostname.clone(), domain: domain.clone(),
..Default::default() ..Default::default()
}, },
..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 { 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 // wait a moment for the backend to start
tokio::time::sleep(Duration::from_millis(5000)).await; tokio::time::sleep(Duration::from_millis(5000)).await;
@ -139,8 +144,6 @@ impl IbisInstance {
username: username.to_string(), username: username.to_string(),
password: "hunter2".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(); api_client.register(form).await.unwrap();
Self { Self {
api_client, api_client,

View file

@ -1,11 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
extern crate ibis_lib;
mod common; mod common;
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT}; use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use ibis_lib::{ use ibis::{
common::{ common::{
utils::extract_domain, utils::extract_domain,
ArticleView, ArticleView,