From 88ecd7ac0092b4387dd3f1cbf7afed136613edd2 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 05:54:32 -0300 Subject: [PATCH 01/61] Run a full build in CI instead of just check --- .forgejo/workflows/check.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index 03f5e32..f305c0f 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -11,13 +11,16 @@ jobs: run: rustup component add rustfmt clippy - name: checkout code uses: actions/checkout@v6 + - name: install dependencies run: cargo install --path . - - name: cargo check - run: cargo check --all-targets + - name: cargo build + run: cargo build --all-targets + - name: format run: cargo fmt -- --check - name: lint run: cargo clippy -- -Dwarnings - name: test run: cargo test + From 9a2a182ea8e0a6eaa748f972b01b4d67b9ca55e5 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 06:29:26 -0300 Subject: [PATCH 02/61] Drop separate "install dependencies" step from CI --- .forgejo/workflows/check.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index f305c0f..bf3a8f2 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -12,8 +12,6 @@ jobs: - name: checkout code uses: actions/checkout@v6 - - name: install dependencies - run: cargo install --path . - name: cargo build run: cargo build --all-targets From f811c3319295181246374af366e64c9c5adbb699 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 06:29:37 -0300 Subject: [PATCH 03/61] Update justfile with a fixed development address --- .justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.justfile b/.justfile index 871c43b..f34bf85 100644 --- a/.justfile +++ b/.justfile @@ -6,7 +6,7 @@ _default: # Build on changes [group('dev')] serve-watch: - bacon --job run-long + bacon --job run-long -- localhost:3003 alias sw := serve-watch alias dev := serve-watch @@ -37,7 +37,7 @@ push: check # Start server [group('run')] serve: - cargo run + cargo run localhost:3003 alias s := serve From 2040854e82c0980df1e8c20d34eb6533b5240d2a Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 15:08:55 -0300 Subject: [PATCH 04/61] Handle CLI arguments as structured argument-parameter pairs --- .justfile | 4 +-- src/formats.rs | 8 ++++-- src/handlers/fixed.rs | 4 ++- src/handlers/graph.rs | 8 ++---- src/main.rs | 10 +++---- src/syntax.rs | 1 + src/syntax/arguments.rs | 63 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 src/syntax.rs create mode 100644 src/syntax/arguments.rs diff --git a/.justfile b/.justfile index f34bf85..19b6f7f 100644 --- a/.justfile +++ b/.justfile @@ -6,7 +6,7 @@ _default: # Build on changes [group('dev')] serve-watch: - bacon --job run-long -- localhost:3003 + bacon --job run-long -- -- --host localhost --port 3003 alias sw := serve-watch alias dev := serve-watch @@ -37,7 +37,7 @@ push: check # Start server [group('run')] serve: - cargo run localhost:3003 + cargo run -- --hostname localhost --port 3003 alias s := serve diff --git a/src/formats.rs b/src/formats.rs index 2054962..28eff67 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -1,9 +1,13 @@ use std::collections::HashMap; -use crate::types::{Graph, Node, Edge}; +use crate::{ + syntax::arguments::Arguments, + types::{Edge, Graph, Node}, +}; pub fn populate_graph() -> Graph { - let toml_source = match std::fs::read_to_string("./static/graph.toml") { + let args = Arguments::new().parse(); + let toml_source = match std::fs::read_to_string(args.graph_path) { Ok(s) => s, Err(e) => format!("Error: {e}"), }; diff --git a/src/handlers/fixed.rs b/src/handlers/fixed.rs index c9b0fb0..a74093c 100644 --- a/src/handlers/fixed.rs +++ b/src/handlers/fixed.rs @@ -3,7 +3,9 @@ use axum::{ http::{Response, StatusCode, header, HeaderValue}, }; -use crate::formats::{Format, populate_graph, serialize_graph}; +use crate::{ + formats::{Format, populate_graph, serialize_graph}, +}; use crate::handlers; pub async fn file(file_path: &str, content_type: &str) -> Response { diff --git a/src/handlers/graph.rs b/src/handlers/graph.rs index dfabf82..8b16b69 100644 --- a/src/handlers/graph.rs +++ b/src/handlers/graph.rs @@ -1,16 +1,14 @@ use axum::{body::Body, extract::Path, http::Response}; -use crate::{formats::populate_graph, types::Node, handlers}; +use crate::{formats::populate_graph, handlers, types::Node}; pub async fn node(Path(id): Path) -> Response { let mut context = tera::Context::new(); let graph = populate_graph(); - let nodes = graph.nodes; - let empty_node = - Node::new(Some(format!("Could not find node with ID {id}."))); + let empty_node = Node::new(Some(format!("Could not find node ID {id}."))); - let node: &Node = nodes.get(&id).unwrap_or(&empty_node); + let node: &Node = graph.nodes.get(&id).unwrap_or(&empty_node); context.insert("id", &id); context.insert("title", &node.title); diff --git a/src/main.rs b/src/main.rs index edeb0c7..1d637e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{backtrace, env, io, panic, sync, time}; +use std::{backtrace, io, panic, sync, time}; use axum::{routing::get, Router}; @@ -7,6 +7,7 @@ use formats::Format; mod formats; mod types; mod handlers; +mod syntax; mod dev; static ONSET: sync::LazyLock = @@ -14,9 +15,8 @@ static ONSET: sync::LazyLock = #[tokio::main] async fn main() -> io::Result<()> { - let args: Vec = env::args().collect(); - let default_address = "0.0.0.0:0".to_string(); - let address: &String = args.get(1).unwrap_or(&default_address); + let args = syntax::arguments::Arguments::new().parse(); + let address = args.make_address(); panic::set_hook(Box::new(|info| { let payload = info @@ -73,7 +73,7 @@ async fn main() -> io::Result<()> { .fallback(handlers::error::not_found); let listener = - tokio::net::TcpListener::bind(address).await.map_err(|e| { + tokio::net::TcpListener::bind(&address).await.map_err(|e| { dev::log( &main, &format!("Failed to create listener at {address}: {e:#?}"), diff --git a/src/syntax.rs b/src/syntax.rs new file mode 100644 index 0000000..d08cc22 --- /dev/null +++ b/src/syntax.rs @@ -0,0 +1 @@ +pub mod arguments; diff --git a/src/syntax/arguments.rs b/src/syntax/arguments.rs new file mode 100644 index 0000000..4226474 --- /dev/null +++ b/src/syntax/arguments.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct Arguments { + pub hostname: String, + pub port: u16, + pub graph_path: PathBuf, +} + +impl Arguments { + pub fn make_address(&self) -> String { + format!("{}:{}", self.hostname, self.port) + } + + pub fn new() -> Arguments { + Arguments { + hostname: String::from("0.0.0.0"), + port: 0, + graph_path: PathBuf::from("./static/graph.toml"), + } + } + + pub fn parse(&self) -> Arguments { + let args: Vec = std::env::args().collect(); + parse(self, &args) + } +} + +fn parse(defaults: &Arguments, args: &[String]) -> Arguments { + let mut out_args = defaults.clone(); + + let filtered_args = if let Some((head, tail)) = args.split_first() { + if head.starts_with('-') { args } else { tail } + } else { + args + }; + + for arg in filtered_args.chunks(2) { + if let Some(argument) = arg.first() + && let Some(parameter) = arg.last() + { + if argument.eq("-h") || argument.eq("--hostname") { + out_args.hostname = String::from(parameter); + } else if argument.eq("-p") || argument.eq("--port") { + out_args.port = parameter.parse().unwrap_or(out_args.port); + } else if argument.eq("-g") || argument.eq("--graph") { + out_args.graph_path = PathBuf::from(parameter); + } else { + crate::dev::log( + &parse, + &format!("Dropped unrecognized argument {argument}"), + ); + } + } else { + crate::dev::log( + &parse, + "Dropped: Couldn't pair either one of or + both argument \"{argument}\", parameter \"{parameter}\"", + ); + } + } + out_args +} From 294c15baa66355a4f507494328ae72517fcb9d7c Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 15:26:49 -0300 Subject: [PATCH 05/61] Fix argparsing error, path in file not found log, panic on invalid args --- .justfile | 2 +- src/handlers/fixed.rs | 2 +- src/syntax/arguments.rs | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.justfile b/.justfile index 19b6f7f..d4c94c6 100644 --- a/.justfile +++ b/.justfile @@ -6,7 +6,7 @@ _default: # Build on changes [group('dev')] serve-watch: - bacon --job run-long -- -- --host localhost --port 3003 + bacon --job run-long -- -- --hostname localhost --port 3003 alias sw := serve-watch alias dev := serve-watch diff --git a/src/handlers/fixed.rs b/src/handlers/fixed.rs index a74093c..0c969f9 100644 --- a/src/handlers/fixed.rs +++ b/src/handlers/fixed.rs @@ -12,7 +12,7 @@ pub async fn file(file_path: &str, content_type: &str) -> Response { let content = match std::fs::read(file_path) { Ok(s) => s, Err(e) => { - panic!("[file_handler] Failed to read file contents: {e}") + panic!("Failed to read {file_path} contents: {e}") }, }; diff --git a/src/syntax/arguments.rs b/src/syntax/arguments.rs index 4226474..c92dd48 100644 --- a/src/syntax/arguments.rs +++ b/src/syntax/arguments.rs @@ -37,7 +37,7 @@ fn parse(defaults: &Arguments, args: &[String]) -> Arguments { for arg in filtered_args.chunks(2) { if let Some(argument) = arg.first() - && let Some(parameter) = arg.last() + && let Some(parameter) = arg.get(1) { if argument.eq("-h") || argument.eq("--hostname") { out_args.hostname = String::from(parameter); @@ -52,11 +52,7 @@ fn parse(defaults: &Arguments, args: &[String]) -> Arguments { ); } } else { - crate::dev::log( - &parse, - "Dropped: Couldn't pair either one of or - both argument \"{argument}\", parameter \"{parameter}\"", - ); + panic!("Argument {arg:?} has no corresponding value") } } out_args From 2e84410bff615aab8cd6f620cbf6639c21b80d42 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 16:22:06 -0300 Subject: [PATCH 06/61] Replace the sample graph with a graph about en itself --- static/graph.toml | 105 ++++++++-------------------------------------- 1 file changed, 18 insertions(+), 87 deletions(-) diff --git a/static/graph.toml b/static/graph.toml index ac70b0b..b9c813c 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -1,97 +1,28 @@ -root_node = "Interface" +root_node = "en" -[nodes.Interface] +[nodes.en] text = """ -An interface is a point of contact between the inside and the outside of something. Contrast with intraface. +en is a tool to write non-linear, connected pieces of text and have their references mapped out as a graph of connected information. + +It works by ingesting a TOML file containing your node specification and serving it as a website that allows nodes to be browsed, searched and listed in relation to each other or as a shallow tree of nodes. """ -links = ["Intraface"] +links = [ "Graph", "TOML" ] -[nodes.Intraface] +[nodes.Graph] text = """ -The intraface is the reflexive process of communicating, creating, thinking, that does not or cannot get shared with others. Contrast with interface. +A graph is a data structure composed of connected (and disconnected) nodes. + +A familiar example is that of a social network. Each account can be thought of as a node and the "follow" and "follower" relationships can be thought of as edges (connections). A node may have many or few connections, and the nodes it is connected to are meaningful to understand how it fits into the whole. + +en uses this concept to create a writing tool, allowing you to map out complex thoughts as a web connected texts. """ -links = ["Thinking", "Interface"] - -[nodes.Thinking] +[nodes.TOML] text = """ -Thinking is a process by which some beings create and manipulate mental constructs. +TOML is a configuration format that can be easily read and understood by humans and machines alike. + +To learn more about TOML, you can visit its website at . + +To see the TOML declaration that translates into the rendered graph you are reading right now, visit the "TOML Graph" link on the top navigation bar. """ - -[nodes.Paradigm] -text = """ -A paradigm is a cohesive set of beliefs, methods and principles that serve both as justification for a given position and as guidance for how to pursue its praxis. -""" - -links = [ "Principle", "Belief", "Method", "Position", "Praxis" ] - -[nodes.Principle] -text = """ -A principle is a belief that implies commitment and necessity. - -Principles change, but to change one's principles too constantly defeats its purpose. - -A principle is usually informed by experience or formed by cultural context, namely religion. - -As other beliefs, simply identifying with a principle does not mean one follows it, which can introduce a sense of dissonance and/or guilt. -""" - -links = [ "Dissonance", "Guilt", "Belief", "Religion", ] - -[[nodes.Principle.connections]] -anchor = "identifying" -to = "Identity" - -[nodes.Religion] -text = """ -A religion is a paradigm that involves unfalsifiable beliefs, particularly those in the domain of morality. - -A reductive critique of religion dismisses it based on its dogmatic adherence to certain beliefs usually rooted in tradition. - -As a counterpoint, consider that the fact religion carries false beliefs does not imply all of the beliefs that religion carries are false, which is an assertion that holds for any other entity. The beliefs that compose the episteme of a religion may include both falsifiable and unfalsifiable beliefs. In this sense, it does not differ from any other paradigm. - -Religion does not subsist solely because of its hard truths, but because it caters to various other basic human necessities: community, identity, knowledge regarding how to conduct one's life. This gives religion enormous presence in society and allows it to act as a strong political force against societal changes that contradict its positions. - -One poignant fact about religion is that its dogmas tend to create exclusion, segregating those who can or want to adhere to them from those who can't or don't want to. This not only means religion will create divisions, it also means that the people excluded from it will be left with less means to fulfill the previously mentioned basic human necessities that religion addresses. -""" - -links = [ - "Paradigm", - "Principle", - "Truth", - "Tradition", - "Morality", - "Episteme", - "Necessity", - "Knowledge", - "Community", - "Identity", - "Dogma", - "Reductionism", -] - -[nodes.Identity] -text = """ -Identity is how individuals construe their sameness and otherness from each other and from nothingness. -""" - -links = ["Principle"] - -[[nodes.Identity.connections]] -anchor = "nothingness" -to = "Emptiness" - -[nodes.Emptiness] -text = """ -Emptiness is the vacuous base in which entities exist. -""" - -links = [ "Entity" ] - -[nodes.Entity] -text = """ -An entity is anything except for actual emptiness. It does not have to be sentient, or physical. It can be an idea, a concept, a memory. The concept of emptiness is an entity, but emptiness itself is not. -""" - -links = [ "Emptiness" ] From d649fd412a1953c5fd038f7f8405faa43da46ff7 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 16:26:05 -0300 Subject: [PATCH 07/61] Add homepage link to Cargo.toml, update roadmap --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5795ff2..2d0c8ec 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ It works by ingesting a TOML file containing your node specification and serving ## Roadmap -- [ ] Anchor rendering - - [ ] Automatic anchors +- [ ] Richer text formatting + - [ ] Anchor rendering + - [ ] Automatic anchors - [ ] Connection kinds - [ ] Reduce O(n) calls in the formats module - [ ] Add tests From effa990e6d2bc7fdb9a2308abfdc0b9e8e497b43 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 17:14:16 -0300 Subject: [PATCH 08/61] Add documentation to sample graph --- Cargo.toml | 2 ++ static/graph.toml | 82 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a76d7b1..1c1a46f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ description = "A non-linear writing instrument." license = "AGPL-3.0-only" repository = "https://codeberg.org/jutty/en" +homepage = "https://en.jutty.dev" +documentation = "https://en.jutty.dev/node/Documentation" edition = "2024" rust-version= "1.91.1" diff --git a/static/graph.toml b/static/graph.toml index b9c813c..79f58ee 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -1,4 +1,84 @@ -root_node = "en" +root_node = "Documentation" + +[nodes.Documentation] +text = """ + +Installation + +For now, if you want to try en, you must build it yourself. + +In an environment with a Rust toolchain and Git installed, run: + +git clone https://codeberg.org/jutty/en +cd en +cargo build --release + +The en binary will be in target/release/en. + +Graph Syntax + +The graph is a TOML file. You can create nodes by adding text such as: + +[nodes.Computer] +text = "A computer is a machine capable of executing user-supplied instructions." + +If you need longer text, it's more convenient to use triple-quoted syntax: + +[nodes.Computer] +text = \""" +A computer is a machine capable of executing user-supplied instructions. +\""" + +Nodes can have connections between each other. + +To add a simple connection without any associated properties, you can simply add links: + +[nodes.Quark] +text = "A subatomic particle that forms hadrons." + +links = [ "Particle", "Hadron" ] + +This will create two outgoing connections from Quark: to Particle and to Hadron. It will also list Quark as an incoming connection in these nodes' pages. + + If you want to add properties to the connection, you can use the connection syntax: + +[[nodes.Quark.connections]] +to = "Particle physics" +anchor = "particle" + +This will create a connection from Quark to "Particle physics", and the first occurrence of the word "particle" in the text of Quark gets anchored to this connection. + +CLI Options + +You can set the hostname, port and graph file path using CLI options: + +For the hostname, use -h or --hostname: + +en -h localhost +en --hostname 10.120.0.5 + +If unspecified, the default is 0.0.0.0. + +For the port, use -p or --port: + +en -p 3003 +en --port 3000 + +If unspecified, the default is to use a random port assigned by the operating system. + +For the graph path, use -g or --graph: + +en -g graph.toml +en --g ./static/my-graph.toml + +If unspecified, the default is ./static/graph.toml. + +You can combine these options as you wish: + +en -h localhost -p 3000 +en --host localhost -p 3003 --graph ./graph.toml + +""" [nodes.en] text = """ From 99d05223d7ad7ac178b7c982c7c0ea9fe2e71805 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 18:09:41 -0300 Subject: [PATCH 09/61] Drop cargo "all targets" flag in CI build --- .forgejo/workflows/check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index bf3a8f2..e7c1afa 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v6 - name: cargo build - run: cargo build --all-targets + run: cargo build - name: format run: cargo fmt -- --check From d9a6938eb6fb67eac5b77c3cc77443efaa549453 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 19:00:57 -0300 Subject: [PATCH 10/61] Add lib.rs, scaffold testing --- .justfile | 14 ++++++++++++++ Cargo.toml | 2 -- src/formats.rs | 9 +++++++++ src/handlers/error.rs | 1 + src/handlers/fixed.rs | 2 ++ src/handlers/graph.rs | 1 + src/handlers/navigation.rs | 1 + src/lib.rs | 10 ++++++++++ src/main.rs | 22 +++++++++++----------- src/syntax/arguments.rs | 3 ++- tests/smoke.rs | 6 ++++++ 11 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 src/lib.rs create mode 100644 tests/smoke.rs diff --git a/.justfile b/.justfile index d4c94c6..6ac8864 100644 --- a/.justfile +++ b/.justfile @@ -19,6 +19,13 @@ test-watch: alias tw := test-watch +# Run cargo check on changes +[group('dev')] +check-watch: + bacon --job check + +alias cw := check-watch + # Format check on changes [group('dev')] format-watch: @@ -26,6 +33,13 @@ format-watch: alias fw := format-watch +# Lint on changes +[group('dev')] +lint-watch: + bacon --job clippy-all + +alias lw := lint-watch + # Check before push [group('dev')] push: check diff --git a/Cargo.toml b/Cargo.toml index 1c1a46f..173a0cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,6 @@ mismatching_type_param_order = "warn" missing_errors_doc = "warn" missing_fields_in_debug = "warn" missing_panics_doc = "warn" -must_use_candidate = "warn" mut_mut = "warn" naive_bytecount = "warn" needless_continue = "warn" @@ -202,7 +201,6 @@ shadow_unrelated = "warn" single_char_lifetime_names = "warn" string_add = "warn" string_lit_chars_any = "warn" -tests_outside_test_module = "warn" unnecessary_self_imports = "warn" unneeded_field_pattern = "warn" unseparated_literal_suffix = "warn" diff --git a/src/formats.rs b/src/formats.rs index 28eff67..6620fec 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -124,3 +124,12 @@ pub fn deserialize_graph(in_format: &Format, serial: &str) -> Graph { }, } } + +#[cfg(test)] +mod tests { + #[test] + fn smoke() { + let n = true; + assert!(n); + } +} diff --git a/src/handlers/error.rs b/src/handlers/error.rs index 0aaa543..a475d23 100644 --- a/src/handlers/error.rs +++ b/src/handlers/error.rs @@ -45,6 +45,7 @@ fn make_body(code: Option, message: Option<&str>) -> String { .0 } +#[expect(clippy::unused_async)] pub async fn not_found() -> Response { by_code( Some(404), diff --git a/src/handlers/fixed.rs b/src/handlers/fixed.rs index 0c969f9..a9c2300 100644 --- a/src/handlers/fixed.rs +++ b/src/handlers/fixed.rs @@ -8,6 +8,8 @@ use crate::{ }; use crate::handlers; +/// # Panics +/// Will panic if file read fails. pub async fn file(file_path: &str, content_type: &str) -> Response { let content = match std::fs::read(file_path) { Ok(s) => s, diff --git a/src/handlers/graph.rs b/src/handlers/graph.rs index 8b16b69..209e9b6 100644 --- a/src/handlers/graph.rs +++ b/src/handlers/graph.rs @@ -2,6 +2,7 @@ use axum::{body::Body, extract::Path, http::Response}; use crate::{formats::populate_graph, handlers, types::Node}; +#[expect(clippy::unused_async)] pub async fn node(Path(id): Path) -> Response { let mut context = tera::Context::new(); diff --git a/src/handlers/navigation.rs b/src/handlers/navigation.rs index cdfdec7..fb4444a 100644 --- a/src/handlers/navigation.rs +++ b/src/handlers/navigation.rs @@ -20,6 +20,7 @@ pub async fn nexus(template: &str) -> Response { handlers::template::by_filename(template, &context, 500, None, false) } +#[expect(clippy::unused_async)] pub async fn search(Form(query): Form) -> Redirect { Redirect::permanent(format!("/node/{}", query.node).as_str()) } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a284a81 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +use std::{sync, time}; + +pub mod formats; +pub mod types; +pub mod handlers; +pub mod syntax; +pub mod dev; + +pub static ONSET: sync::LazyLock = + sync::LazyLock::new(time::Instant::now); diff --git a/src/main.rs b/src/main.rs index 1d637e8..a0e128a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,8 @@ -use std::{backtrace, io, panic, sync, time}; +use std::{backtrace, io, panic}; use axum::{routing::get, Router}; -use formats::Format; - -mod formats; -mod types; -mod handlers; -mod syntax; -mod dev; - -static ONSET: sync::LazyLock = - sync::LazyLock::new(time::Instant::now); +use en::{ONSET, handlers, syntax, dev, formats::Format}; #[tokio::main] async fn main() -> io::Result<()> { @@ -99,3 +90,12 @@ async fn main() -> io::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + #[test] + fn smoke() { + let e = true; + assert!(e); + } +} diff --git a/src/syntax/arguments.rs b/src/syntax/arguments.rs index c92dd48..7ae13e7 100644 --- a/src/syntax/arguments.rs +++ b/src/syntax/arguments.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Arguments { pub hostname: String, pub port: u16, @@ -20,6 +20,7 @@ impl Arguments { } } + #[must_use] pub fn parse(&self) -> Arguments { let args: Vec = std::env::args().collect(); parse(self, &args) diff --git a/tests/smoke.rs b/tests/smoke.rs new file mode 100644 index 0000000..70e3018 --- /dev/null +++ b/tests/smoke.rs @@ -0,0 +1,6 @@ +#[test] +fn add() { + let e = 0_i32; + let n = 0_i32; + assert_eq!(e, n); +} From ddc09e84c9fcdb63f358e641e966f5fa6e976436 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 19:11:59 -0300 Subject: [PATCH 11/61] Trigger build for CD testing --- static/graph.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/graph.toml b/static/graph.toml index 79f58ee..1436cd3 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -20,7 +20,7 @@ Graph Syntax The graph is a TOML file. You can create nodes by adding text such as: [nodes.Computer] -text = "A computer is a machine capable of executing user-supplied instructions." +text = "A computer is a machine capable of executing arbitrary instructions." If you need longer text, it's more convenient to use triple-quoted syntax: From c2a10512c87834fb222a0bfa2b42d95b82e5e3e8 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 20:52:31 -0300 Subject: [PATCH 12/61] Update documentation link on about page --- templates/about.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/about.html b/templates/about.html index 9c7d15a..31bffe1 100644 --- a/templates/about.html +++ b/templates/about.html @@ -13,7 +13,7 @@ From f14ee592afb2b92507afb0b24b93633f9635750b Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 21:01:48 -0300 Subject: [PATCH 13/61] Remove 'no connections' message, minor docs tweaks --- static/graph.toml | 5 +++-- templates/node.html | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/static/graph.toml b/static/graph.toml index 1436cd3..78a3053 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -26,7 +26,7 @@ If you need longer text, it's more convenient to use triple-quoted syntax: [nodes.Computer] text = \""" -A computer is a machine capable of executing user-supplied instructions. +A computer is a machine capable of executing arbitrary instructions. \""" Nodes can have connections between each other. @@ -76,7 +76,8 @@ If unspecified, the default is ./static/graph.toml. You can combine these options as you wish: en -h localhost -p 3000 -en --host localhost -p 3003 --graph ./graph.toml +en -p 3003 --host localhost --graph ./graph.toml +en --g ./graph.toml -p 1312 """ diff --git a/templates/node.html b/templates/node.html index 21d461a..cd7f759 100644 --- a/templates/node.html +++ b/templates/node.html @@ -47,7 +47,5 @@ {% endif %} - {% else %} - Node has no connections. {% endif %} {%- endblock body %} From 089b507299451cdbcd5b7ceacb36277e85ab1229 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 14 Dec 2025 21:17:14 -0300 Subject: [PATCH 14/61] Extract router from main to its own module --- src/handlers/error.rs | 1 - src/handlers/graph.rs | 1 - src/handlers/navigation.rs | 1 - src/lib.rs | 1 + src/main.rs | 40 ++----------------------------------- src/router.rs | 41 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 src/router.rs diff --git a/src/handlers/error.rs b/src/handlers/error.rs index a475d23..0aaa543 100644 --- a/src/handlers/error.rs +++ b/src/handlers/error.rs @@ -45,7 +45,6 @@ fn make_body(code: Option, message: Option<&str>) -> String { .0 } -#[expect(clippy::unused_async)] pub async fn not_found() -> Response { by_code( Some(404), diff --git a/src/handlers/graph.rs b/src/handlers/graph.rs index 209e9b6..8b16b69 100644 --- a/src/handlers/graph.rs +++ b/src/handlers/graph.rs @@ -2,7 +2,6 @@ use axum::{body::Body, extract::Path, http::Response}; use crate::{formats::populate_graph, handlers, types::Node}; -#[expect(clippy::unused_async)] pub async fn node(Path(id): Path) -> Response { let mut context = tera::Context::new(); diff --git a/src/handlers/navigation.rs b/src/handlers/navigation.rs index fb4444a..cdfdec7 100644 --- a/src/handlers/navigation.rs +++ b/src/handlers/navigation.rs @@ -20,7 +20,6 @@ pub async fn nexus(template: &str) -> Response { handlers::template::by_filename(template, &context, 500, None, false) } -#[expect(clippy::unused_async)] pub async fn search(Form(query): Form) -> Redirect { Redirect::permanent(format!("/node/{}", query.node).as_str()) } diff --git a/src/lib.rs b/src/lib.rs index a284a81..6fb5eea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use std::{sync, time}; pub mod formats; pub mod types; +pub mod router; pub mod handlers; pub mod syntax; pub mod dev; diff --git a/src/main.rs b/src/main.rs index a0e128a..d690bf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ use std::{backtrace, io, panic}; -use axum::{routing::get, Router}; - -use en::{ONSET, handlers, syntax, dev, formats::Format}; +use en::{ONSET, syntax, dev}; #[tokio::main] async fn main() -> io::Result<()> { @@ -27,41 +25,7 @@ async fn main() -> io::Result<()> { } })); - let app = Router::new() - .route( - "/", - get(|| handlers::navigation::nexus("index.html")) - .post(handlers::navigation::search), - ) - .route( - "/graph/toml", - get(|| handlers::fixed::serial(&Format::Toml)), - ) - .route( - "/graph/json", - get(|| handlers::fixed::serial(&Format::Json)), - ) - .route( - "/static/style.css", - get(|| handlers::fixed::file("./static/style.css", "text/css")), - ) - .route( - "/static/favicon.svg", - get(|| { - handlers::fixed::file("./static/favicon.svg", "image/svg+xml") - }), - ) - .route( - "/node/{node_id}", - get(handlers::graph::node).post(handlers::graph::node), - ) - .route("/tree", get(|| handlers::navigation::nexus("tree.html"))) - .route("/about", get(|| handlers::template::fixed("about.html"))) - .route( - "/acknowledgments", - get(|| handlers::template::fixed("acknowledgments.html")), - ) - .fallback(handlers::error::not_found); + let app = en::router::new(); let listener = tokio::net::TcpListener::bind(&address).await.map_err(|e| { diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..4357b1d --- /dev/null +++ b/src/router.rs @@ -0,0 +1,41 @@ +use axum::{routing::get, Router}; + +use crate::{handlers, formats::Format}; + +pub fn new() -> Router { + Router::new() + .route( + "/", + get(|| handlers::navigation::nexus("index.html")) + .post(handlers::navigation::search), + ) + .route( + "/graph/toml", + get(|| handlers::fixed::serial(&Format::Toml)), + ) + .route( + "/graph/json", + get(|| handlers::fixed::serial(&Format::Json)), + ) + .route( + "/static/style.css", + get(|| handlers::fixed::file("./static/style.css", "text/css")), + ) + .route( + "/static/favicon.svg", + get(|| { + handlers::fixed::file("./static/favicon.svg", "image/svg+xml") + }), + ) + .route( + "/node/{node_id}", + get(handlers::graph::node).post(handlers::graph::node), + ) + .route("/tree", get(|| handlers::navigation::nexus("tree.html"))) + .route("/about", get(|| handlers::template::fixed("about.html"))) + .route( + "/acknowledgments", + get(|| handlers::template::fixed("acknowledgments.html")), + ) + .fallback(handlers::error::not_found) +} From 2f247f477be0c19d4214a09b873d30f0550cc299 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 15 Dec 2025 19:37:35 -0300 Subject: [PATCH 15/61] Add parser for node text with support for headers --- src/handlers/graph.rs | 5 +++- src/syntax.rs | 1 + src/syntax/content.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ static/graph.toml | 11 +++++---- templates/node.html | 4 +-- 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/syntax/content.rs diff --git a/src/handlers/graph.rs b/src/handlers/graph.rs index 8b16b69..d8120f3 100644 --- a/src/handlers/graph.rs +++ b/src/handlers/graph.rs @@ -12,10 +12,13 @@ pub async fn node(Path(id): Path) -> Response { context.insert("id", &id); context.insert("title", &node.title); - context.insert("text", &node.text); context.insert("connections", &node.connections.clone()); context.insert("incoming", &graph.incoming.get(&id)); + let escaped_text = tera::escape_html(&node.text); + let out_text = crate::syntax::content::parse(&escaped_text); + context.insert("text", &out_text); + let not_found = node.clone() == empty_node; let template_name = "node.html".to_string(); diff --git a/src/syntax.rs b/src/syntax.rs index d08cc22..caef4dc 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -1 +1,2 @@ pub mod arguments; +pub mod content; diff --git a/src/syntax/content.rs b/src/syntax/content.rs new file mode 100644 index 0000000..3dba7c9 --- /dev/null +++ b/src/syntax/content.rs @@ -0,0 +1,57 @@ +use std::fmt::Write as _; +use crate::dev::log; + +pub fn parse(text: &str) -> String { + let mut out_text: Vec = Vec::new(); + + for line in text.lines() { + if line.is_empty() || line.replace(" ", "").is_empty() { + continue; + } + + let mut out_line: String = line.to_owned(); + let words: Vec = line.split(" ").map(str::to_string).collect(); + let first_word: &String = + words.first().unwrap_or_else(|| unreachable!()); + + if is_header(first_word) { + out_line = parse_header(&out_line, first_word); + } + // if not special, default to treating line as a paragraph + else { + out_line.insert_str(0, "

"); + out_line.push_str("

"); + } + + out_text.push(out_line); + } + + out_text.join("\n") +} + +fn is_header(lexeme: &str) -> bool { + !lexeme.trim().is_empty() + && lexeme.replace("#", "").is_empty() + && lexeme.len() <= 6 +} + +fn parse_header(line: &str, first_word: &str) -> String { + log(&parse_header, &format!("Parsing: {line:?}")); + + let header_level = first_word.len(); + log(&parse, &format!("Header level is {header_level}")); + let header_text = line.to_owned().replace(first_word, ""); + let mut w = String::with_capacity(header_text.len().strict_add(9)); + let alloc = w.capacity(); + match write!(w, "{header_text}") { + Ok(()) => (), + Err(e) => panic!("{e:?}"), + } + if alloc != w.capacity() { + log( + &parse_header, + &format!("w reallocated to {} despite prediction", w.capacity()), + ); + } + w +} diff --git a/static/graph.toml b/static/graph.toml index 78a3053..a3bdfe1 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -3,7 +3,7 @@ root_node = "Documentation" [nodes.Documentation] text = """ -Installation +## Installation For now, if you want to try en, you must build it yourself. @@ -15,7 +15,7 @@ cargo build --release The en binary will be in target/release/en. -Graph Syntax +## Graph Syntax The graph is a TOML file. You can create nodes by adding text such as: @@ -31,7 +31,8 @@ A computer is a machine capable of executing arbitrary instructions. Nodes can have connections between each other. -To add a simple connection without any associated properties, you can simply add links: +To add a simple connection without any associated properties, you can simply +add links: [nodes.Quark] text = "A subatomic particle that forms hadrons." @@ -40,7 +41,7 @@ links = [ "Particle", "Hadron" ] This will create two outgoing connections from Quark: to Particle and to Hadron. It will also list Quark as an incoming connection in these nodes' pages. - If you want to add properties to the connection, you can use the connection syntax: +If you want to add properties to the connection, you can use the connection syntax: [[nodes.Quark.connections]] to = "Particle physics" @@ -48,7 +49,7 @@ anchor = "particle" This will create a connection from Quark to "Particle physics", and the first occurrence of the word "particle" in the text of Quark gets anchored to this connection. -CLI Options +## CLI Options You can set the hostname, port and graph file path using CLI options: diff --git a/templates/node.html b/templates/node.html index cd7f759..6a6e39a 100644 --- a/templates/node.html +++ b/templates/node.html @@ -6,9 +6,7 @@

{{ title }}

ID: {{ id }} - {% for line in text | split(pat="\n") %} - {% if line %}

{{ line }}

{% endif %} - {% endfor %} + {{ text | safe }}
{% if connections or incoming %}