From 817777d7d6ce656c05d1468207a8dd3afc0cbef6 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 18 Jan 2026 03:10:30 -0300 Subject: [PATCH 001/108] Expand Graph module test coverage --- src/graph.rs | 370 ++++++++++++++++++++++++++++++++++++++++++++-- src/graph/edge.rs | 1 + static/graph.toml | 4 +- 3 files changed, 357 insertions(+), 18 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 8a3e01c..b9344e4 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -22,7 +22,9 @@ pub mod meta; #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] pub struct Graph { + #[serde(default)] pub nodes: HashMap, + #[serde(default)] pub root_node: String, #[serde(skip_deserializing)] pub incoming: HashMap>, @@ -37,6 +39,7 @@ pub struct Graph { #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] pub struct Stats { pub detached: HashMap, + pub detached_total: u32, } impl Graph { @@ -63,9 +66,10 @@ impl Graph { graph } - /// Loads a TOML file from the default location and returns a modulated Graph + /// Loads a Graph TOML file from CLI arguments or their defaults and + /// returns a modulated Graph. /// - /// Returns a graph with an error message if any errors are propagated to it. + /// Returns a graph with an error message if any errors are propagated. pub fn load() -> Graph { let result = Graph::load_file(None); match result { @@ -76,12 +80,12 @@ impl Graph { /// Takes a file path to a TOML file and returns a modulated Graph /// - /// If `path` is an empty string, it will fallback to CLI arguments + /// If `path` is None, it will fallback to CLI arguments or their defaults. /// /// # Errors /// Propagates errors from `Graph::read_file`. pub fn load_file(path: Option<&str>) -> Result { - let mut graph = Graph::read_file(path)?; + let mut graph = Graph::from_file(path)?; graph.modulate(); Ok(graph) } @@ -91,7 +95,7 @@ impl Graph { /// # Errors /// Returns Err if it can't read the contents of `in_path`. /// Propagates errors from `Graph::from_serial`. - pub fn read_file(in_path: Option<&str>) -> Result { + pub fn from_file(in_path: Option<&str>) -> Result { let cli_path = Arguments::default().parse().graph_path; let path = in_path.map_or(cli_path, PathBuf::from); @@ -143,19 +147,16 @@ impl Graph { /// # Errors /// Errors on unsupported formats. /// Propagates serialization errors. - pub fn to_serial( - graph: &Graph, - format: &Format, - ) -> Result { + pub fn to_serial(&self, format: &Format) -> Result { match *format { - Format::TOML => match toml::to_string(graph) { + Format::TOML => match toml::to_string(self) { Ok(s) => Ok(s), Err(e) => Err(SerialError { cause: SerialErrorCause::MalformedInput, message: e.to_string(), }), }, - Format::JSON => match serde_json::to_string(graph) { + Format::JSON => match serde_json::to_string(self) { Ok(s) => Ok(s), Err(e) => Err(SerialError { cause: SerialErrorCause::MalformedInput, @@ -184,7 +185,7 @@ impl Graph { tlog!(&instant, "Parsed configuration"); } - // Construct a HashMap with incoming connections (reversed edges) + /// Construct a HashMap with incoming connections (reversed edges) fn map_incoming(&mut self) { for node in self.nodes.clone().into_values() { for edge in node.connections.clone().unwrap_or_default().values() { @@ -210,13 +211,18 @@ impl Graph { for (connection_key, edge) in connections { let mut new_edge = edge.clone(); - // Populate empty "from" IDs in edges with node's ID + // Populate empty "to" and "from" IDs if edge.from.is_empty() { - new_edge.from.clone_from(&connection_key); + new_edge.from.clone_from(&key); + } + + if edge.to.is_empty() { + new_edge.to.clone_from(&connection_key); } // Flag detached edges - if in_nodes.contains_key(&edge.to) { + if (!edge.to.is_empty() && in_nodes.contains_key(&edge.to)) || + (edge.to.is_empty() && in_nodes.contains_key(&new_edge.to)) { new_edge.detached = false; } else { new_edge.detached = true; @@ -255,7 +261,7 @@ impl Graph { // Populate empty summaries with the leading part of the node text let summary = if node.summary.is_empty() { let first_line = if let Some(first) = - node.text.lines().find(|s| !s.is_empty()) + node.text.split("\n\n").find(|s| !s.is_empty()) { String::from(first) } else { @@ -295,6 +301,7 @@ impl Graph { let graph = self.clone(); let iterator = self.nodes.iter_mut(); for (key, node) in iterator { + // Parse node text let parse_output = content::rich_parse(&node.text, &graph); node.text = parse_output.text.unwrap_or_default(); @@ -352,6 +359,8 @@ impl Graph { } } } + + } fn increment_detached(&mut self, node_id: &str) { @@ -360,6 +369,7 @@ impl Graph { .entry(node_id.to_string()) .and_modify(|count| *count = count.saturating_add(1)) .or_insert(1); + self.stats.detached_total = self.stats.detached_total.saturating_add(1); } pub fn map_lowercase_keys(&mut self) { @@ -557,6 +567,334 @@ mod tests { e.message.contains("expected value at line 1 column 1") },)); } + + #[test] + fn with_message() { + let payload = "QmMxohuLe9DZCOzcaxH2wzZGqOot1In6"; + let graph = Graph::with_message(payload); + assert_eq!(payload, graph.meta.messages.first().unwrap()); + } + + #[test] + fn malformed_without_message() { + let graph = Graph::malformed(None); + assert!(graph.meta.messages.is_empty()); + } + + #[test] + fn malformed_with_message() { + let payload = "s8LuGwRQA4GdNGvAlaIUrryZYBGkY5Ev"; + let graph = Graph::malformed(Some(payload)); + assert_eq!(payload, graph.meta.messages.first().unwrap()); + } + + #[test] + fn bad_deserial_input() { + let result = Graph::from_serial("not toml", &Format::TOML); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err().cause, SerialErrorCause::MalformedInput)); + } + + #[test] + fn bad_deserial_format() { + let result = Graph::from_serial("not toml", &Format::Unsupported); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err().cause, SerialErrorCause::UnsupportedFormat)); + } + + #[test] + fn bad_serial_format() { + let result = Graph::load().to_serial(&Format::Unsupported); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err().cause, SerialErrorCause::UnsupportedFormat)); + } + + #[test] + fn empty_modulated_graph_is_empty() { + let mut graph = Graph::from_serial("", &Format::TOML).unwrap(); + graph.modulate(); + + assert!(graph.nodes.is_empty()); + assert!(graph.incoming.is_empty()); + } + + #[test] + fn title_population_from_id() { + let mut graph = Graph::from_serial(concat!( + "[nodes.TitlelessNode]\n", + r#"text = "Some text""#, + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + let node = graph.nodes.get("TitlelessNode"); + assert_eq!(node.unwrap().title, "TitlelessNode"); + } + + #[test] + fn no_title_population_from_id_if_title_set() { + let mut graph = Graph::from_serial(concat!( + "[nodes.TitlefulNode]\n", + r#"title = "A Title""#, "\n", + r#"text = "Some text""#, + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + let node = graph.nodes.get("TitlefulNode"); + assert_eq!(node.unwrap().title, "A Title"); + } + + #[test] + fn detached_edge_is_flagged() { + let mut graph = Graph::from_serial(concat!( + "[nodes.Node]\n", + r#"text = "Some text here""#, "\n\n", + "[nodes.Node.connections.Nowhere]\n", + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + let node = graph.nodes.get("Node").unwrap(); + let connections = node.connections.as_ref().unwrap(); + let connection = connections.get("Nowhere").unwrap(); + assert!(connection.detached); + } + + #[test] + fn attached_edge_is_not_flagged() { + let mut graph = Graph::from_serial(concat!( + "[nodes.NodeOne]\n", + r#"text = "Some text here""#, "\n\n", + "[nodes.NodeOne.connections.NodeTwo]\n\n", + "[nodes.NodeTwo]\n", + r#"text = "Some other text here""#, "\n\n", + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + let node = graph.nodes.get("NodeOne").unwrap(); + let connections = node.connections.as_ref().unwrap(); + let connection = connections.get("NodeTwo").unwrap(); + println!("{connection:#?}"); + assert!(!connection.detached); + } + + #[test] + fn to_and_from_population() { + let mut graph = Graph::from_serial(concat!( + "[nodes.n01]\n", + "[nodes.n01.connections.n02]\n\n", + "[nodes.n02]\n", + "[nodes.n02.connections.n03]\n\n", + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + let n01 = graph.nodes.get("n01").unwrap(); + let n02 = graph.nodes.get("n02").unwrap(); + let n01_connections = n01.connections.as_ref().unwrap(); + let n02_connections = n02.connections.as_ref().unwrap(); + let n01_to_n02 = n01_connections.get("n02").unwrap(); + let n02_to_n03 = n02_connections.get("n03").unwrap(); + + assert_eq!(n01_to_n02.from, "n01"); + assert_eq!(n01_to_n02.to, "n02"); + assert!(!n01_to_n02.detached); + assert_eq!(n02_to_n03.from, "n02"); + assert_eq!(n02_to_n03.to, "n03"); + assert!(n02_to_n03.detached); + } + + #[test] + fn links_become_connections() { + let mut graph = Graph::from_serial(concat!( + "[nodes.n01]\n", + r#"links = [ "n02", "n03", "n04" ]"#, "\n\n", + "[nodes.n02]\n", + "[nodes.n04]\n", + r#"links = [ "n01", "n03" ]"#, "\n\n", + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + let n01 = graph.nodes.get("n01").unwrap(); + let n02 = graph.nodes.get("n02").unwrap(); + let n04 = graph.nodes.get("n04").unwrap(); + + let n01_connections = n01.connections.as_ref().unwrap(); + let n02_connections = n02.connections.as_ref().unwrap(); + let n04_connections = n04.connections.as_ref().unwrap(); + + let n01_to_n02 = n01_connections.get("n02").unwrap(); + let n01_to_n03 = n01_connections.get("n03").unwrap(); + let n01_to_n04 = n01_connections.get("n04").unwrap(); + + let n04_to_n01 = n04_connections.get("n01").unwrap(); + let n04_to_n03 = n04_connections.get("n03").unwrap(); + + assert_eq!(n01_to_n02.from, "n01"); + assert_eq!(n01_to_n02.to, "n02"); + assert!(!n01_to_n02.detached); + + assert_eq!(n01_to_n03.from, "n01"); + assert_eq!(n01_to_n03.to, "n03"); + assert!(n01_to_n03.detached); + + assert_eq!(n01_to_n04.from, "n01"); + assert_eq!(n01_to_n04.to, "n04"); + assert!(!n01_to_n04.detached); + + assert!(n02_connections.is_empty()); + + assert_eq!(n04_to_n01.from, "n04"); + assert_eq!(n04_to_n01.to, "n01"); + assert!(!n04_to_n01.detached); + + assert_eq!(n04_to_n03.from, "n04"); + assert_eq!(n04_to_n03.to, "n03"); + assert!(n04_to_n03.detached); + } + + #[test] + fn detached_count_increments() { + let mut graph = Graph::from_serial(concat!( + "[nodes.n01]\n", + r#"links = [ "n02", "n03", "n04", "n05", "n06", "n10" ]"#, "\n\n", + "[nodes.n02]\n", + "[nodes.n04]\n", + r#"links = [ "n01", "n02", "n03", "n06", "n11", "n15" ]"#, "\n\n"), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + assert_eq!(graph.stats.detached_total, 8); + } + + #[test] + fn populated_summary() { + let text = "vh18qEUN22X2SxLj6lpOOzMBB4N6S0UG"; + let mut graph = Graph::from_serial(&format!( + "[nodes.n01]\n\ + text = \"{text}\"\n\ + "), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + assert!(graph.nodes.get("n01").unwrap().summary.contains(text)); + assert!(graph.nodes.get("n01").unwrap().text.contains(text)); + } + + #[test] + fn supplied_summary() { + let text = "vh18qEUN22X2SxLj6lpOOzMBB4N6S0UG"; + let summary = "W5dhPgNs7S1Zsq6uPK47MAw8xXyNxwep"; + let mut graph = Graph::from_serial(&format!( + "[nodes.n01]\n\ + summary = \"{summary}\"\n\ + text = \"{text}\"\n\ + ", + ), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + assert_eq!(graph.nodes.get("n01").unwrap().summary, summary); + assert!(graph.nodes.get("n01").unwrap().text.contains(text)); + } + + #[test] + fn summary_from_first_sentence() { + let first_sentence = "zTWFX0a8 tYTO2g."; + let text = format!( + "{first_sentence} QoGa PtDsJ vh18qE U N22 X2S. MBB4N6S0UG\n\n\ + 6FokUX o OCEc LzZFfR1nkqa hWIF LdrtD3G. PDQwv Ba2PnZ yEBVpqQdt\n\n\ + Py6aoPK FV7iU UdrYB vD UeMvvg u 5kbt 9ZW9x7MR" + ); + let mut graph = Graph::from_serial(&format!( + "[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n"), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + assert_eq!(graph.nodes.get("n01").unwrap().summary, first_sentence); + } + + #[test] + fn summary_from_first_paragraph() { + let first_paragraph = "EGR6IS fTQsRp rv7 g jvItnYU 2HNciS MID\n\ + iz3vx vXxa vW4JI6l E5itd6qm2Yx gFw 1D Nq0805 bXHe3h iqABe ilnHKl\n\ + F4AHvMLto cz3C Z279r9 jtIbBnY JqjwZPQdepf cdv6"; + let text = format!( + "{first_paragraph}\n\nn0D CvHcIU7R oPcZy V1Iy9PgXO gOw lfeDy\n\n\ + jrOSq 0uVtJLd Idx08Bpy BBj 4PVS R9lt RqjTs s AURUx93 Xu9WiI0rP.\n" + ); + let mut graph = Graph::from_serial( + format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n").as_str(), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + assert_eq!(graph.nodes.get("n01").unwrap().summary, first_paragraph); + } + + #[test] + fn summary_from_first_300_chars() { + let first_300 = concat!("Primis, quod, cum in rerum natura duo ", + "quaerenda sint, unum, quae materia sit, ex qua quaeque res ", + "efficiatur, alterum, quae naturales essent nec tamen id, cuius ", + "causa haec finxerat, assecutus est: Nam si omnes veri erunt, ut ", + "Epicuri ratio docet, tum denique poterit aliquid cognosci et ", + "percipi? Quos q"); + let tail = concat!("uam autem et praeterita grate meminit et ", + "praesentibus ita potitur, ut animadvertat quanta sint ea ", + "quamque iucunda, neque pendet ex futuris, sed expectat illa, ", + "fruitur praesentibus ab iisque vitii"); + let text = format!("{first_300}{tail}"); + let summary = format!("{first_300}…"); + + let mut graph = Graph::from_serial( + format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n").as_str(), + &Format::TOML, + ).unwrap(); + graph.modulate(); + + assert_eq!(graph.nodes.get("n01").unwrap().summary, summary); + } + + #[test] + fn anchors_become_connections() { + + let mut graph = Graph::from_serial("\ + [nodes.n1] + text = 'an anchor to |n2|, the existing node' + + [nodes.n2] + text = 'an anchor to |n0|, the nonexistent node' + + ", &Format::TOML, + ).unwrap(); + graph.modulate(); + + let n1_to_n2 = graph.nodes.get("n1").unwrap() + .connections.as_ref().unwrap().get("n2"); + + let n2_to_n0 = graph.nodes.get("n2").unwrap() + .connections.as_ref().unwrap().get("n0"); + + println!("{n1_to_n2:#?}"); + println!("{n2_to_n0:#?}"); + + assert!(!n1_to_n2.unwrap().detached); + assert!(n2_to_n0.unwrap().detached); + } } #[cfg(test)] diff --git a/src/graph/edge.rs b/src/graph/edge.rs index e95e45c..02961ac 100644 --- a/src/graph/edge.rs +++ b/src/graph/edge.rs @@ -2,6 +2,7 @@ use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] pub struct Edge { + #[serde(default)] pub to: String, #[serde(default)] pub from: String, diff --git a/static/graph.toml b/static/graph.toml index 491d246..2ac5285 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -376,7 +376,7 @@ If you want to render a literal backslash, you can escape the backslash itself b ## Interactions with TOML escaping -Notice that TOML itself also handles escape codes, so to pass a backslash you will need to escape it as a double backslash inside strings delimited by double quotes or triple double quotes. You can use a single backslash inside a string delimited by single quotes: +Notice that |TOML| itself also handles escape codes, so to pass a backslash you will need to escape it as a double backslash inside strings delimited by double quotes or triple double quotes. You can use a single backslash inside a string delimited by single quotes: ` [node.Double] @@ -396,7 +396,7 @@ Here too: \\\\* ''' ` -This has nothing to do with en's markup syntax per se, it's just a consequence of how backslashes are also special in |TOML| syntax. For more details, see the |TOML documentation on Strings|https://toml.io/en/v1.1.0#string|. +This has nothing to do with en's markup syntax per se, it's just a consequence of how backslashes are also special in TOML syntax. For more details, see the |TOML documentation on Strings|https://toml.io/en/v1.1.0#string|. ## Interactions with HTML From c0e518297841878be7de3d00e2c026f75ebd1f17 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:39:09 -0300 Subject: [PATCH 002/108] Return Result instead of Option in version parsing --- src/graph/meta.rs | 430 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 383 insertions(+), 47 deletions(-) diff --git a/src/graph/meta.rs b/src/graph/meta.rs index bea1b2b..eb7da88 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -1,10 +1,12 @@ +use crate::prelude::*; + use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct Meta { pub config: Config, #[serde(default)] - pub version: Option, + pub version: Version, #[serde(default)] pub messages: Vec, #[serde(default)] @@ -15,7 +17,7 @@ impl Default for Meta { fn default() -> Meta { Meta { config: Config::default(), - version: Version::from_env(), + version: Version::from_compilation().unwrap_or_default(), messages: vec![], malformed: false, } @@ -113,6 +115,191 @@ fn mk8() -> u16 { 8 } +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Version { + major: u8, + minor: u8, + patch: u8, +} + +impl Default for Version { + fn default() -> Version { + match Version::from_compilation() { + Ok(v) => v, + Err(e) => { + log!(ERROR, "{e}"); + Version { + major: 0, + minor: 0, + patch: 0, + } + }, + } + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Version { + pub fn from_compilation() -> Result { + Version::from_str(env!("CARGO_PKG_VERSION")) + } + + pub fn from_str(version: &str) -> Result { + use VersionErrorCause::*; + + let triple: Vec = + version.split('.').map(str::to_string).collect(); + + let has_two_dots = version.matches('.').count() == 2; + let has_three_elements = triple.len() == 3; + let has_whitespace = version.contains(' ') || version.contains('\n'); + let has_contiguous_dots = version.contains(".."); + let ends_with_dot = version.ends_with('.'); + let starts_with_dot = version.starts_with('.'); + + let major: u8 = if let Some(s) = triple.first() + && !s.is_empty() + { + match s.trim_start_matches('v').trim().parse() { + Ok(parsed) => parsed, + Err(e) => { + return Err(VersionError { + cause: FailedMajorParse, + message: Some(e.to_string()), + }); + }, + } + } else { + return Err(VersionError { + cause: MissingMajor, + message: None, + }); + }; + + let minor: u8 = if triple.len() >= 2 + && let Some(s) = triple.get(1) + { + match s.trim().parse() { + Ok(parsed) => parsed, + Err(e) => { + return Err(VersionError { + cause: FailedMinorParse, + message: Some(e.to_string()), + }); + }, + } + } else { + return Err(VersionError { + cause: MissingMinor, + message: None, + }); + }; + + let patch: u8 = if triple.len() >= 3 + && let Some(s) = triple.get(2) + { + match s.trim().parse() { + Ok(parsed) => parsed, + Err(e) => { + return Err(VersionError { + cause: FailedPatchParse, + message: Some(e.to_string()), + }); + }, + } + } else { + return Err(VersionError { + cause: MissingPatch, + message: None, + }); + }; + + let conditions = has_two_dots + && has_three_elements + && !has_whitespace + && !has_contiguous_dots + && !ends_with_dot + && !starts_with_dot; + + if conditions { + Ok(Version { + major, + minor, + patch, + }) + } else { + Err(VersionError { + cause: FailedValidation, + message: Some(format!( + "Evaluated rules (all must be true):\n\ + Has exactly two dots: {has_two_dots},\n\ + Splits to three elements: {has_three_elements}\n\ + Has no whitespace: {}\n\ + Has no contiguous dots: {}\n\ + Does not end with a dot: {}\n\ + Does not start with a dot: {}", + !has_whitespace, + !has_contiguous_dots, + !ends_with_dot, + !starts_with_dot, + )), + }) + } + } +} + +#[derive(Debug)] +pub struct VersionError { + cause: VersionErrorCause, + message: Option, +} + +#[derive(Debug)] +enum VersionErrorCause { + MissingMajor, + FailedMajorParse, + MissingMinor, + FailedMinorParse, + MissingPatch, + FailedPatchParse, + FailedValidation, +} + +impl std::fmt::Display for VersionError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(message) = &self.message { + write!(f, "{}: {}", self.cause, message) + } else { + write!(f, "{}", self.cause) + } + } +} + +impl std::fmt::Display for VersionErrorCause { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use VersionErrorCause as V; + + write!( + f, + "{}", + match self { + V::MissingMajor => "Major version couldn't be split", + V::FailedMajorParse => "Failed parse of major version", + V::MissingMinor => "Minor version couldn't be split", + V::FailedMinorParse => "Failed parse of minor version", + V::MissingPatch => "Patch version couldn't be split", + V::FailedPatchParse => "Failed parse of patch version", + V::FailedValidation => "Validation failed", + } + ) + } +} + #[cfg(test)] mod tests { use crate::graph::Graph; @@ -179,60 +366,209 @@ mod tests { == 1 ); } -} -#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] -pub struct Version { - major: u8, - minor: u8, - patch: u8, -} + #[test] + fn modulated_graph_version_matches_compilation_version() { + let version = Graph::load().meta.version; -impl Version { - pub fn from_env() -> Option { - Version::from(env!("CARGO_PKG_VERSION")) + assert_eq!(format!("{version}"), env!("CARGO_PKG_VERSION")); } - pub fn from(version: &str) -> Option { - let triple: Vec = - version.split('.').map(str::to_string).collect(); + #[test] + fn from_compilation_matches_compilation_version() { + let version = Version::from_compilation().unwrap(); - let has_two_dots = version.matches('.').count() == 2; - let has_three_elements = triple.len() == 3; - let has_whitespace = version.contains(' ') || version.contains('\n'); - let has_contiguous_dots = version.contains(".."); - let ends_with_dot = version.ends_with('.'); - let starts_with_dot = version.starts_with('.'); + assert_eq!(format!("{version}"), env!("CARGO_PKG_VERSION")); + } - let major: u8 = if let Some(s) = triple.first() { - s.trim_start_matches('v').trim().parse().ok()? - } else { - return None; - }; + #[test] + fn version_from_str() { + let payload = "3.9.74"; + let version_result = Version::from_str(payload); - let minor: u8 = if let Some(s) = triple.get(1) { - s.trim().parse().ok()? - } else { - return None; - }; + println!("{version_result:#?}"); + assert_eq!(format!("{}", version_result.unwrap()), payload); + } - let patch: u8 = if let Some(s) = triple.get(2) { - s.trim().parse().ok()? - } else { - return None; - }; + #[test] + fn missing_major() { + let error = Version::from_str("").unwrap_err(); + println!("{error:#?}"); + assert!(matches!(error.cause, VersionErrorCause::MissingMajor)); + } - let conditions = has_two_dots - && has_three_elements - && !has_whitespace - && !has_contiguous_dots - && !ends_with_dot - && !starts_with_dot; + #[test] + fn missing_minor() { + let error = Version::from_str("3").unwrap_err(); + println!("{error:#?}"); + assert!(matches!(error.cause, VersionErrorCause::MissingMinor)); + } - conditions.then_some(Version { - major, - minor, - patch, - }) + #[test] + fn missing_patch() { + let error = Version::from_str("3.6").unwrap_err(); + assert!(matches!(error.cause, VersionErrorCause::MissingPatch)); + } + + #[test] + fn malformed_patch() { + let error = Version::from_str("3.6.x").unwrap_err(); + assert!(matches!(error.cause, VersionErrorCause::FailedPatchParse)); + + let error_empty = Version::from_str("3.6.").unwrap_err(); + assert!(matches!( + error_empty.cause, + VersionErrorCause::FailedPatchParse + )); + } + + #[test] + fn malformed_minor() { + let error = Version::from_str("3.x.0").unwrap_err(); + assert!(matches!(error.cause, VersionErrorCause::FailedMinorParse)); + + let error_bad_patch = Version::from_str("3.x.z").unwrap_err(); + assert!(matches!( + error_bad_patch.cause, + VersionErrorCause::FailedMinorParse + )); + + let error_empty_patch = Version::from_str("3.x.").unwrap_err(); + assert!(matches!( + error_empty_patch.cause, + VersionErrorCause::FailedMinorParse + )); + + let error_patchless = Version::from_str("3.x").unwrap_err(); + assert!(matches!( + error_patchless.cause, + VersionErrorCause::FailedMinorParse + )); + + let error_empty = Version::from_str("3.").unwrap_err(); + assert!(matches!( + error_empty.cause, + VersionErrorCause::FailedMinorParse + )); + } + + #[test] + fn malformed_major() { + let error = Version::from_str("x.6.0").unwrap_err(); + assert!(matches!(error.cause, VersionErrorCause::FailedMajorParse)); + + let error_bad_patch = Version::from_str("x.y.z").unwrap_err(); + assert!(matches!( + error_bad_patch.cause, + VersionErrorCause::FailedMajorParse + )); + + let error_empty_patch = Version::from_str("x.6.").unwrap_err(); + assert!(matches!( + error_empty_patch.cause, + VersionErrorCause::FailedMajorParse + )); + + let error_patchless = Version::from_str("x.6").unwrap_err(); + assert!(matches!( + error_patchless.cause, + VersionErrorCause::FailedMajorParse + )); + + let error_bad_minor = Version::from_str("x.y").unwrap_err(); + assert!(matches!( + error_bad_minor.cause, + VersionErrorCause::FailedMajorParse + )); + + let error_empty_minor = Version::from_str("x.").unwrap_err(); + assert!(matches!( + error_empty_minor.cause, + VersionErrorCause::FailedMajorParse + )); + + let error_minorless = Version::from_str("x").unwrap_err(); + assert!(matches!( + error_minorless.cause, + VersionErrorCause::FailedMajorParse + )); + + let error_empty = Version::from_str("").unwrap_err(); + assert!(matches!(error_empty.cause, VersionErrorCause::MissingMajor)); + } + + #[test] + fn version_validation() { + assert!(["3.1.4.", "3.1.4.1"].iter().all(|s| matches!( + Version::from_str(s).unwrap_err().cause, + VersionErrorCause::FailedValidation + ))); + } + + #[test] + fn leading_v() { + let version = Version::from_str("v3.1.4").unwrap(); + assert_eq!(format!("{version}"), "3.1.4"); + } + + #[test] + fn display_version_error_cause() { + fn assert(version: &str, message: &str) { + let error = Version::from_str(version).unwrap_err(); + assert_eq!(format!("{error}"), message); + } + + assert("3.6", "Patch version couldn't be split"); + assert( + "3.6.", + "Failed parse of patch version: \ + cannot parse integer from empty string", + ); + assert( + "3.6.x", + "Failed parse of patch version: \ + invalid digit found in string", + ); + + assert("3", "Minor version couldn't be split"); + assert( + "3.", + "Failed parse of minor version: \ + cannot parse integer from empty string", + ); + assert( + "3.x", + "Failed parse of minor version: \ + invalid digit found in string", + ); + + assert("", "Major version couldn't be split"); + assert( + "x", + "Failed parse of major version: \ + invalid digit found in string", + ); + + let validation_error = Version::from_str("3.1.4.1..").unwrap_err(); + println!("{validation_error}"); + + assert!(matches!( + validation_error.cause, + VersionErrorCause::FailedValidation + )); + assert!( + validation_error + .message + .clone() + .unwrap() + .contains("Has exactly two dots: false") + ); + assert!( + validation_error + .message + .clone() + .unwrap() + .contains("Splits to three elements: false") + ); } } From 9f9042a19de6e179ec7030fbf71d9b93b56ecfcf Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:42:23 -0300 Subject: [PATCH 003/108] Make node connections no longer an Option --- src/graph.rs | 56 +++++++++++++++-------------------------------- src/graph/node.rs | 6 ++--- 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index b9344e4..5a3fc08 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -188,7 +188,7 @@ impl Graph { /// Construct a HashMap with incoming connections (reversed edges) fn map_incoming(&mut self) { for node in self.nodes.clone().into_values() { - for edge in node.connections.clone().unwrap_or_default().values() { + for edge in node.connections.clone().values() { let mut edges = self .incoming .get(&edge.to.clone()) @@ -204,7 +204,7 @@ impl Graph { let in_nodes = self.nodes.clone(); for (key, node) in in_nodes.clone() { - let connections = node.connections.clone().unwrap_or_default(); + let connections = node.connections.clone(); let mut new_edges = connections.clone(); // Modulate connections @@ -289,7 +289,7 @@ impl Graph { id: key.clone(), title: new_title, summary: flatten(&summary, self), - connections: Some(new_edges), + connections: new_edges, ..node.clone() }; @@ -304,23 +304,12 @@ impl Graph { // Parse node text let parse_output = content::rich_parse(&node.text, &graph); - node.text = parse_output.text.unwrap_or_default(); + node.text + .clone_from(&parse_output.text.clone().unwrap_or_default()); // Create connections for each anchor - let mut parsed_anchor_tokens = parse_output - .tokens - .iter() - .filter(|t| matches!(t, Token::Anchor(_))) - .cloned() - .collect::>(); - parsed_anchor_tokens.extend_from_slice( - &parse_output - .format_tokens - .iter() - .filter(|t| matches!(t, Token::Anchor(_))) - .cloned() - .collect::>(), - ); + let parsed_anchors = + parse_output.only(&Token::Anchor(Box::default())); let mut anchors: Vec = vec![]; for token in parsed_anchor_tokens { if let Token::Anchor(token_data) = token { @@ -678,8 +667,7 @@ mod tests { graph.modulate(); let node = graph.nodes.get("NodeOne").unwrap(); - let connections = node.connections.as_ref().unwrap(); - let connection = connections.get("NodeTwo").unwrap(); + let connection = node.connections.get("NodeTwo").unwrap(); println!("{connection:#?}"); assert!(!connection.detached); } @@ -698,10 +686,8 @@ mod tests { let n01 = graph.nodes.get("n01").unwrap(); let n02 = graph.nodes.get("n02").unwrap(); - let n01_connections = n01.connections.as_ref().unwrap(); - let n02_connections = n02.connections.as_ref().unwrap(); - let n01_to_n02 = n01_connections.get("n02").unwrap(); - let n02_to_n03 = n02_connections.get("n03").unwrap(); + let n01_to_n02 = n01.connections.get("n02").unwrap(); + let n02_to_n03 = n02.connections.get("n03").unwrap(); assert_eq!(n01_to_n02.from, "n01"); assert_eq!(n01_to_n02.to, "n02"); @@ -728,16 +714,12 @@ mod tests { let n02 = graph.nodes.get("n02").unwrap(); let n04 = graph.nodes.get("n04").unwrap(); - let n01_connections = n01.connections.as_ref().unwrap(); - let n02_connections = n02.connections.as_ref().unwrap(); - let n04_connections = n04.connections.as_ref().unwrap(); + let n01_to_n02 = n01.connections.get("n02").unwrap(); + let n01_to_n03 = n01.connections.get("n03").unwrap(); + let n01_to_n04 = n01.connections.get("n04").unwrap(); - let n01_to_n02 = n01_connections.get("n02").unwrap(); - let n01_to_n03 = n01_connections.get("n03").unwrap(); - let n01_to_n04 = n01_connections.get("n04").unwrap(); - - let n04_to_n01 = n04_connections.get("n01").unwrap(); - let n04_to_n03 = n04_connections.get("n03").unwrap(); + let n04_to_n01 = n04.connections.get("n01").unwrap(); + let n04_to_n03 = n04.connections.get("n03").unwrap(); assert_eq!(n01_to_n02.from, "n01"); assert_eq!(n01_to_n02.to, "n02"); @@ -751,7 +733,7 @@ mod tests { assert_eq!(n01_to_n04.to, "n04"); assert!(!n01_to_n04.detached); - assert!(n02_connections.is_empty()); + assert!(n02.connections.is_empty()); assert_eq!(n04_to_n01.from, "n04"); assert_eq!(n04_to_n01.to, "n01"); @@ -883,11 +865,9 @@ mod tests { ).unwrap(); graph.modulate(); - let n1_to_n2 = graph.nodes.get("n1").unwrap() - .connections.as_ref().unwrap().get("n2"); + let n1_to_n2 = graph.nodes.get("n1").unwrap().connections.get("n2"); - let n2_to_n0 = graph.nodes.get("n2").unwrap() - .connections.as_ref().unwrap().get("n0"); + let n2_to_n0 = graph.nodes.get("n2").unwrap().connections.get("n0"); println!("{n1_to_n2:#?}"); println!("{n2_to_n0:#?}"); diff --git a/src/graph/node.rs b/src/graph/node.rs index 6d2071f..ec79d24 100644 --- a/src/graph/node.rs +++ b/src/graph/node.rs @@ -21,8 +21,8 @@ pub struct Node { #[serde(default)] pub hidden: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub connections: Option>, + #[serde(default)] + pub connections: HashMap, #[serde(default)] pub stats: Stats, @@ -43,7 +43,7 @@ impl Node { Some(s) => s, None => "Node not found.".to_string(), }, - connections: None, + connections: HashMap::default(), links: vec![], redirect: String::default(), hidden: false, From 2549e904b35e092706f606ae9e0aceb01bda1d32 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:43:40 -0300 Subject: [PATCH 004/108] Cleanup node display logic --- src/graph/node.rs | 106 +++++++++++++++++++++++++------- src/router/handlers/template.rs | 2 +- 2 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/graph/node.rs b/src/graph/node.rs index ec79d24..672f835 100644 --- a/src/graph/node.rs +++ b/src/graph/node.rs @@ -35,7 +35,7 @@ pub struct Stats { } impl Node { - pub fn new(message: Option) -> Node { + pub fn not_found(message: Option) -> Node { Node { id: "404".to_string(), title: "Not Found".to_string(), @@ -55,35 +55,36 @@ impl Node { impl std::fmt::Display for Node { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let mut meta = String::default(); - if self.title.is_empty() { - meta.push_str("title:none"); - } else { - meta.push_str(&format!("title:'{}'", self.title)); + let mut meta_elements: Vec = vec![]; + + if !self.title.is_empty() { + meta_elements.push(format!("title:'{}'", self.title)); } - if self.text.is_empty() { - meta.push_str(" text:none"); - } else { - meta.push_str(&format!(" text:{}l", self.text.len())); + + if !self.text.is_empty() { + meta_elements.push(format!("text:{}l", self.text.len())); } - if self.summary.is_empty() { - meta.push_str(" summary:none"); - } else { - meta.push_str(&format!(" summary:{}", self.summary.len())); + + if !self.summary.is_empty() { + meta_elements.push(format!("summary:{}", self.summary.len())); } - if self.redirect.is_empty() { - meta.push_str(" redirect:none"); - } else { - meta.push_str(&format!(" redirect:{}", self.redirect)); + + if !self.redirect.is_empty() { + meta_elements.push(format!("redirect:{}", self.redirect)); } + let links = self.links.len(); if links > 0 { - meta.push_str(&format!(" links:{links}")); + meta_elements.push(format!("links:{links}")); } + if self.hidden { - meta.push_str(" hidden"); + meta_elements.push(String::from("hidden")); } - write!(f, "Node: ID '{}' {meta}", self.id) + + let meta = meta_elements.join(" "); + + write!(f, "Node {} [{meta}]", self.id) } } @@ -93,7 +94,68 @@ mod tests { #[test] fn empty_node_message() { - let node = Node::new(None); + let node = Node::not_found(None); assert_eq!(node.text, "Node not found."); } + + #[test] + fn display() { + let mut node = Node::not_found(None); + assert_eq!(format!("{node}"), "Node 404 [title:'Not Found' text:15l]"); + + let summary = "X2hSwanDoLdqLZNnYJagcWKFJVAx5TGF"; + node.summary = String::from(summary); + assert_eq!( + format!("{node}"), + format!( + "Node 404 [title:'Not Found' text:15l summary:{}]", + summary.len() + ) + ); + + let redirect = "ukfF3kz130oUzT2ushBIvEHx8xoY8ke0"; + node.redirect = String::from(redirect); + assert_eq!( + format!("{node}"), + format!( + "Node 404 [title:'Not Found' text:15l summary:{} redirect:{redirect}]", + summary.len(), + ) + ); + + node.links.push(String::from("1")); + node.links.push(String::from("2")); + node.links.push(String::from("3")); + + assert_eq!( + format!("{node}"), + format!( + "Node 404 [\ + title:'Not Found' \ + text:15l summary:{} \ + redirect:{redirect} \ + links:{}\ + ]", + summary.len(), + node.links.len(), + ) + ); + + node.hidden = true; + + assert_eq!( + format!("{node}"), + format!( + "Node 404 [\ + title:'Not Found' \ + text:15l summary:{} \ + redirect:{redirect} \ + links:{} \ + hidden\ + ]", + summary.len(), + node.links.len(), + ) + ); + } } diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index 5ccecc9..c590424 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -176,7 +176,7 @@ mod tests { fn render_with_context() { let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy"; let mut context = tera::Context::default(); - let node = crate::graph::Node::new(Some(payload.to_string())); + let node = crate::graph::Node::not_found(Some(payload.to_string())); let graph = Graph::load(); context.insert("node", &node); context.insert("graph", &graph); From 386e6b482bf197b2bfa648d7145a3af1eb355e95 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:45:40 -0300 Subject: [PATCH 005/108] Fix summing of total detached edges --- src/graph.rs | 64 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 5a3fc08..bcd4e11 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -170,6 +170,11 @@ impl Graph { } } + fn gather_stats(&mut self) { + let detached = self.stats.detached.values(); + self.stats.detached_total = detached.sum(); + } + pub fn modulate(&mut self) { let mut instant = now(); instant = tlog!(&instant, "Started node modulation"); @@ -181,6 +186,8 @@ impl Graph { instant = tlog!(&instant, "Modulated edges"); self.map_incoming(); instant = tlog!(&instant, "Mapped incoming edges"); + self.gather_stats(); + instant = tlog!(&instant, "Gathered stats"); self.parse_config(); tlog!(&instant, "Parsed configuration"); } @@ -221,8 +228,10 @@ impl Graph { } // Flag detached edges - if (!edge.to.is_empty() && in_nodes.contains_key(&edge.to)) || - (edge.to.is_empty() && in_nodes.contains_key(&new_edge.to)) { + if (!edge.to.is_empty() && in_nodes.contains_key(&edge.to)) + || (edge.to.is_empty() + && in_nodes.contains_key(&new_edge.to)) + { new_edge.detached = false; } else { new_edge.detached = true; @@ -311,7 +320,7 @@ impl Graph { let parsed_anchors = parse_output.only(&Token::Anchor(Box::default())); let mut anchors: Vec = vec![]; - for token in parsed_anchor_tokens { + for token in parsed_anchors { if let Token::Anchor(token_data) = token { anchors.push(*token_data.clone()); } @@ -319,27 +328,33 @@ impl Graph { for anchor in anchors { if let Some(anchor_node) = anchor.node() { - if let Some(ref mut connections) = node.connections { - connections.insert( - anchor_node.id.clone(), - Edge { - from: key.clone(), - to: anchor_node.id, - detached: false, - }, - ); - } + node.connections.insert( + anchor_node.id.clone(), + Edge { + from: key.clone(), + to: anchor_node.id, + detached: false, + }, + ); } else { if let Some(destination) = anchor.destination() && !anchor.external() { + let trimmed_destination = destination + .trim_start_matches("/node/") + .to_string(); + node.connections.insert( + trimmed_destination.clone(), + Edge { + from: key.clone(), + to: trimmed_destination.clone(), + detached: true, + }, + ); + self.stats .detached - .entry( - destination - .trim_start_matches("/node/") - .to_string(), - ) + .entry(trimmed_destination) .and_modify(|count| { *count = count.saturating_add(1); }) @@ -348,8 +363,6 @@ impl Graph { } } } - - } fn increment_detached(&mut self, node_id: &str) { @@ -358,7 +371,6 @@ impl Graph { .entry(node_id.to_string()) .and_modify(|count| *count = count.saturating_add(1)) .or_insert(1); - self.stats.detached_total = self.stats.detached_total.saturating_add(1); } pub fn map_lowercase_keys(&mut self) { @@ -417,7 +429,11 @@ impl Graph { redirect: true, } } else { - QueryResult::default() + QueryResult { + node: None, + exact: false, + redirect: true, + } } } else { log!(VERBOSE, "Returning candidate {candidate}"); @@ -443,7 +459,7 @@ pub enum Format { Unsupported, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum SerialErrorCause { UnsupportedFormat, MalformedInput, @@ -459,7 +475,7 @@ impl std::fmt::Display for SerialErrorCause { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct SerialError { pub cause: SerialErrorCause, pub message: String, From 5151c53a2bc2fface99ee6dde7011aef497fae04 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:45:54 -0300 Subject: [PATCH 006/108] Expand test coverage --- .justfile | 2 +- Cargo.toml | 1 - src/graph.rs | 519 ++++++++++++++++--- src/log/level.rs | 113 +++- src/router/handlers/fixed.rs | 9 + src/router/handlers/graph.rs | 34 +- src/router/handlers/mime.rs | 49 ++ src/syntax/content.rs | 40 ++ src/syntax/content/parser/token.rs | 2 +- src/syntax/content/parser/token/anchor.rs | 37 +- src/syntax/content/parser/token/bold.rs | 6 + src/syntax/content/parser/token/checkbox.rs | 6 + src/syntax/content/parser/token/header.rs | 26 + src/syntax/content/parser/token/item.rs | 6 + src/syntax/content/parser/token/list.rs | 19 + src/syntax/content/parser/token/oblique.rs | 6 + src/syntax/content/parser/token/preformat.rs | 6 + src/syntax/content/parser/token/strike.rs | 6 + src/syntax/content/parser/token/underline.rs | 6 + static/public/assets/style.css | 1 + 20 files changed, 773 insertions(+), 121 deletions(-) diff --git a/.justfile b/.justfile index cd3f175..ad3be8c 100644 --- a/.justfile +++ b/.justfile @@ -263,7 +263,7 @@ export CARGO_TERM_COLOR := 'always' debug_vars := 'DEBUG=${DEBUG:-} DEBUG_FILTER=${DEBUG_FILTER:-} RUST_BACKTRACE=${RUST_BACKTRACE:-} RUSTFLAGS=${RUSTFLAGS:-}' watch_cmd := "watchexec -qc -r -e rs,toml,html --color always -- " -cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|dev\.rs"' +cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|log\.rs"' just_cmd := 'just --timestamp --explain --command-color green' set unstable diff --git a/Cargo.toml b/Cargo.toml index 16a37ef..6b60a1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,6 @@ same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" set_contains_or_insert = "warn" should_panic_without_expect = "warn" -similar_names = "warn" str_split_at_newline = "warn" struct_field_names = "warn" trivially_copy_pass_by_ref = "warn" diff --git a/src/graph.rs b/src/graph.rs index bcd4e11..e94c936 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -192,7 +192,7 @@ impl Graph { tlog!(&instant, "Parsed configuration"); } - /// Construct a HashMap with incoming connections (reversed edges) + /// Construct a `HashMap` with incoming connections (reversed edges) fn map_incoming(&mut self) { for node in self.nodes.clone().into_values() { for edge in node.connections.clone().values() { @@ -310,7 +310,6 @@ impl Graph { let graph = self.clone(); let iterator = self.nodes.iter_mut(); for (key, node) in iterator { - // Parse node text let parse_output = content::rich_parse(&node.text, &graph); node.text @@ -504,7 +503,7 @@ impl std::fmt::Display for Format { match self { Format::TOML => write!(f, "TOML"), Format::JSON => write!(f, "JSON"), - Format::Unsupported => write!(f, "Unsupported"), + Format::Unsupported => write!(f, "Unsupported format"), } } } @@ -518,7 +517,15 @@ pub struct QueryResult { impl std::fmt::Display for QueryResult { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let meta = if self.redirect { "[redirect] " } else { "" }; + let meta = if self.redirect && self.exact { + "[exact redirect] " + } else if !self.redirect && self.exact { + "[exact] " + } else if self.redirect && !self.exact { + "[redirect] " + } else { + "" + }; let node = if let Some(n) = &self.node { n.id.clone() } else { @@ -597,21 +604,30 @@ mod tests { fn bad_deserial_input() { let result = Graph::from_serial("not toml", &Format::TOML); assert!(result.is_err()); - assert!(matches!(result.unwrap_err().cause, SerialErrorCause::MalformedInput)); + assert!(matches!( + result.unwrap_err().cause, + SerialErrorCause::MalformedInput + )); } #[test] fn bad_deserial_format() { let result = Graph::from_serial("not toml", &Format::Unsupported); assert!(result.is_err()); - assert!(matches!(result.unwrap_err().cause, SerialErrorCause::UnsupportedFormat)); + assert!(matches!( + result.unwrap_err().cause, + SerialErrorCause::UnsupportedFormat + )); } #[test] fn bad_serial_format() { let result = Graph::load().to_serial(&Format::Unsupported); assert!(result.is_err()); - assert!(matches!(result.unwrap_err().cause, SerialErrorCause::UnsupportedFormat)); + assert!(matches!( + result.unwrap_err().cause, + SerialErrorCause::UnsupportedFormat + )); } #[test] @@ -625,12 +641,11 @@ mod tests { #[test] fn title_population_from_id() { - let mut graph = Graph::from_serial(concat!( - "[nodes.TitlelessNode]\n", - r#"text = "Some text""#, - ), + let mut graph = Graph::from_serial( + concat!("[nodes.TitlelessNode]\n", r#"text = "Some text""#,), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); let node = graph.nodes.get("TitlelessNode"); @@ -639,13 +654,16 @@ mod tests { #[test] fn no_title_population_from_id_if_title_set() { - let mut graph = Graph::from_serial(concat!( - "[nodes.TitlefulNode]\n", - r#"title = "A Title""#, "\n", - r#"text = "Some text""#, + let mut graph = Graph::from_serial( + concat!( + "[nodes.TitlefulNode]\n", + r#"title = "A Title""#, + "\n", + r#"text = "Some text""#, ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); let node = graph.nodes.get("TitlefulNode"); @@ -654,32 +672,38 @@ mod tests { #[test] fn detached_edge_is_flagged() { - let mut graph = Graph::from_serial(concat!( - "[nodes.Node]\n", - r#"text = "Some text here""#, "\n\n", - "[nodes.Node.connections.Nowhere]\n", + let mut graph = Graph::from_serial( + concat!( + "[nodes.Node]\n", + r#"text = "Some text here""#, + "\n\n", + "[nodes.Node.connections.Nowhere]\n", ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); let node = graph.nodes.get("Node").unwrap(); - let connections = node.connections.as_ref().unwrap(); - let connection = connections.get("Nowhere").unwrap(); + let connection = node.connections.get("Nowhere").unwrap(); assert!(connection.detached); } #[test] fn attached_edge_is_not_flagged() { - let mut graph = Graph::from_serial(concat!( - "[nodes.NodeOne]\n", - r#"text = "Some text here""#, "\n\n", - "[nodes.NodeOne.connections.NodeTwo]\n\n", - "[nodes.NodeTwo]\n", - r#"text = "Some other text here""#, "\n\n", + let mut graph = Graph::from_serial( + concat!( + "[nodes.NodeOne]\n", + r#"text = "Some text here""#, + "\n\n", + "[nodes.NodeOne.connections.NodeTwo]\n\n", + "[nodes.NodeTwo]\n", + r#"text = "Some other text here""#, + "\n\n", ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); let node = graph.nodes.get("NodeOne").unwrap(); @@ -690,14 +714,16 @@ mod tests { #[test] fn to_and_from_population() { - let mut graph = Graph::from_serial(concat!( - "[nodes.n01]\n", - "[nodes.n01.connections.n02]\n\n", - "[nodes.n02]\n", - "[nodes.n02.connections.n03]\n\n", + let mut graph = Graph::from_serial( + concat!( + "[nodes.n01]\n", + "[nodes.n01.connections.n02]\n\n", + "[nodes.n02]\n", + "[nodes.n02.connections.n03]\n\n", ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); let n01 = graph.nodes.get("n01").unwrap(); @@ -715,15 +741,19 @@ mod tests { #[test] fn links_become_connections() { - let mut graph = Graph::from_serial(concat!( - "[nodes.n01]\n", - r#"links = [ "n02", "n03", "n04" ]"#, "\n\n", - "[nodes.n02]\n", - "[nodes.n04]\n", - r#"links = [ "n01", "n03" ]"#, "\n\n", + let mut graph = Graph::from_serial( + concat!( + "[nodes.n01]\n", + r#"links = [ "n02", "n03", "n04" ]"#, + "\n\n", + "[nodes.n02]\n", + "[nodes.n04]\n", + r#"links = [ "n01", "n03" ]"#, + "\n\n", ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); let n01 = graph.nodes.get("n01").unwrap(); @@ -762,14 +792,19 @@ mod tests { #[test] fn detached_count_increments() { - let mut graph = Graph::from_serial(concat!( - "[nodes.n01]\n", - r#"links = [ "n02", "n03", "n04", "n05", "n06", "n10" ]"#, "\n\n", - "[nodes.n02]\n", - "[nodes.n04]\n", - r#"links = [ "n01", "n02", "n03", "n06", "n11", "n15" ]"#, "\n\n"), + let mut graph = Graph::from_serial( + concat!( + "[nodes.n01]\n", + r#"links = [ "n02", "n03", "n04", "n05", "n06", "n10" ]"#, + "\n\n", + "[nodes.n02]\n", + "[nodes.n04]\n", + r#"links = [ "n01", "n02", "n03", "n06", "n11", "n15" ]"#, + "\n\n" + ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); assert_eq!(graph.stats.detached_total, 8); @@ -778,12 +813,15 @@ mod tests { #[test] fn populated_summary() { let text = "vh18qEUN22X2SxLj6lpOOzMBB4N6S0UG"; - let mut graph = Graph::from_serial(&format!( - "[nodes.n01]\n\ + let mut graph = Graph::from_serial( + &format!( + "[nodes.n01]\n\ text = \"{text}\"\n\ - "), + " + ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); assert!(graph.nodes.get("n01").unwrap().summary.contains(text)); @@ -794,14 +832,16 @@ mod tests { fn supplied_summary() { let text = "vh18qEUN22X2SxLj6lpOOzMBB4N6S0UG"; let summary = "W5dhPgNs7S1Zsq6uPK47MAw8xXyNxwep"; - let mut graph = Graph::from_serial(&format!( - "[nodes.n01]\n\ + let mut graph = Graph::from_serial( + &format!( + "[nodes.n01]\n\ summary = \"{summary}\"\n\ text = \"{text}\"\n\ ", ), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); assert_eq!(graph.nodes.get("n01").unwrap().summary, summary); @@ -816,10 +856,11 @@ mod tests { 6FokUX o OCEc LzZFfR1nkqa hWIF LdrtD3G. PDQwv Ba2PnZ yEBVpqQdt\n\n\ Py6aoPK FV7iU UdrYB vD UeMvvg u 5kbt 9ZW9x7MR" ); - let mut graph = Graph::from_serial(&format!( - "[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n"), + let mut graph = Graph::from_serial( + &format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n"), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); assert_eq!(graph.nodes.get("n01").unwrap().summary, first_sentence); @@ -837,7 +878,8 @@ mod tests { let mut graph = Graph::from_serial( format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n").as_str(), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); assert_eq!(graph.nodes.get("n01").unwrap().summary, first_paragraph); @@ -845,23 +887,28 @@ mod tests { #[test] fn summary_from_first_300_chars() { - let first_300 = concat!("Primis, quod, cum in rerum natura duo ", + let first_300 = concat!( + "Primis, quod, cum in rerum natura duo ", "quaerenda sint, unum, quae materia sit, ex qua quaeque res ", "efficiatur, alterum, quae naturales essent nec tamen id, cuius ", "causa haec finxerat, assecutus est: Nam si omnes veri erunt, ut ", "Epicuri ratio docet, tum denique poterit aliquid cognosci et ", - "percipi? Quos q"); - let tail = concat!("uam autem et praeterita grate meminit et ", + "percipi? Quos q" + ); + let tail = concat!( + "uam autem et praeterita grate meminit et ", "praesentibus ita potitur, ut animadvertat quanta sint ea ", "quamque iucunda, neque pendet ex futuris, sed expectat illa, ", - "fruitur praesentibus ab iisque vitii"); + "fruitur praesentibus ab iisque vitii" + ); let text = format!("{first_300}{tail}"); let summary = format!("{first_300}…"); let mut graph = Graph::from_serial( format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n").as_str(), &Format::TOML, - ).unwrap(); + ) + .unwrap(); graph.modulate(); assert_eq!(graph.nodes.get("n01").unwrap().summary, summary); @@ -869,16 +916,18 @@ mod tests { #[test] fn anchors_become_connections() { - - let mut graph = Graph::from_serial("\ + let mut graph = Graph::from_serial( + "\ [nodes.n1] text = 'an anchor to |n2|, the existing node' [nodes.n2] text = 'an anchor to |n0|, the nonexistent node' - ", &Format::TOML, - ).unwrap(); + ", + &Format::TOML, + ) + .unwrap(); graph.modulate(); let n1_to_n2 = graph.nodes.get("n1").unwrap().connections.get("n2"); @@ -891,6 +940,334 @@ mod tests { assert!(!n1_to_n2.unwrap().detached); assert!(n2_to_n0.unwrap().detached); } + + #[test] + fn detached_anchors_increase_counts() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + text = 'this |anchor| is detached, as is |this one|.'\n\ + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + assert_eq!(graph.stats.detached_total, 2); + assert_eq!(*graph.stats.detached.get("anchor").unwrap(), 1); + assert_eq!(*graph.stats.detached.get("this one").unwrap(), 1); + } + + #[test] + fn repeated_detached_anchors_increase_counts() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + text = 'this |anchor| is detached, as appears twice: |anchor|.'\n\ + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + assert_eq!(graph.stats.detached_total, 2); + assert_eq!(*graph.stats.detached.get("anchor").unwrap(), 2); + } + + #[test] + fn find_exact() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + let query_result = graph.find_node("n1"); + assert_eq!(query_result.node.unwrap().id, "n1"); + assert!(query_result.exact); + assert!(!query_result.redirect); + } + + #[test] + fn find_inexact() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + let query_result = graph.find_node("n 1"); + println!("{query_result}"); + + assert_eq!(query_result.node.unwrap().id, "n1"); + assert!(!query_result.exact); + assert!(!query_result.redirect); + } + + #[test] + fn find_inexact_to_redirect() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + redirect = 'n3' + \n\ + [nodes.n3] + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + let query_result = graph.find_node("n 1"); + println!("{query_result}"); + + assert_eq!(query_result.node.unwrap().id, "n3"); + assert!(!query_result.exact); + assert!(query_result.redirect); + } + + #[test] + fn double_recursion_to_redirect() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + redirect = 'n2' + \n\ + [nodes.n2]\n\ + redirect = 'n 3' + \n\ + [nodes.n3] + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + let query_result = graph.find_node("n 1"); + println!("{query_result}"); + + assert_eq!(query_result.node.unwrap().id, "n3"); + assert!(!query_result.exact); + assert!(query_result.redirect); + } + + #[test] + fn find_redirect_to_inexisting() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + redirect = 'n0'\n\ + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + let query_result = graph.find_node("n1"); + println!("{query_result}"); + + assert!(query_result.node.is_none()); + assert!(query_result.redirect); + assert!(!query_result.exact); + } + + #[test] + fn find_redirect_to_existing() { + let mut graph = Graph::from_serial( + "\ + [nodes.n1]\n\ + redirect = 'n2'\n\ + \n\ + [nodes.n2] + ", + &Format::TOML, + ) + .unwrap(); + graph.modulate(); + + let query_result = graph.find_node("n1"); + assert_eq!(query_result.node.unwrap().id, "n2"); + assert!(query_result.redirect); + assert!(!query_result.exact); + } + + #[test] + fn serial_error_display() { + let bad_input = SerialErrorCause::MalformedInput; + let bad_format = SerialErrorCause::UnsupportedFormat; + + assert_eq!(format!("{bad_input}"), "Malformed Input"); + assert_eq!(format!("{bad_format}"), "Unsupported Format"); + } + + #[test] + fn string_from_serial_error() { + let bad_input_error_message = "denSehpfhCjr05gUd7TgYLb8veJHAMZW"; + let bad_input_cause = SerialErrorCause::MalformedInput; + let bad_input = SerialError { + cause: bad_input_cause.clone(), + message: bad_input_error_message.to_string(), + }; + + let bad_format_error_message = "4brcCkWOgLHBvhLk2OcgTOKQgpKrc1bB"; + let bad_format_cause = SerialErrorCause::UnsupportedFormat; + let bad_format = SerialError { + cause: bad_format_cause.clone(), + message: bad_format_error_message.to_string(), + }; + + let s_bad_input = String::from(bad_input.clone()); + let s_bad_format = String::from(bad_format.clone()); + + assert!(s_bad_input.contains(bad_input_error_message)); + assert!(s_bad_input.contains(bad_input.message.as_str())); + assert!(s_bad_input.contains(format!("{bad_input_cause}").as_str())); + + assert!(s_bad_format.contains(bad_format_error_message)); + assert!(s_bad_format.contains(bad_format.message.as_str())); + assert!(s_bad_format.contains(format!("{bad_format_cause}").as_str())); + } + + #[test] + fn format_from_str() { + let uppercase_toml = Format::from("TOML"); + let lowercase_toml = Format::from("toml"); + let mixed_case_toml = Format::from("tOmL"); + + assert!(matches!(uppercase_toml, Format::TOML)); + assert!(matches!(lowercase_toml, Format::TOML)); + assert!(matches!(mixed_case_toml, Format::TOML)); + + let uppercase_json = Format::from("JSON"); + let lowercase_json = Format::from("json"); + let mixed_case_json = Format::from("JsoN"); + + assert!(matches!(uppercase_json, Format::JSON)); + assert!(matches!(lowercase_json, Format::JSON)); + assert!(matches!(mixed_case_json, Format::JSON)); + + let unsupported = [ + Format::from("j son"), + Format::from(""), + Format::from("strawberry"), + Format::from(" "), + Format::from("\n"), + ]; + + assert!(unsupported.iter().all(|f| matches!(f, Format::Unsupported))); + } + + #[test] + fn format_display() { + let toml = format!("{}", Format::TOML); + let json = format!("{}", Format::JSON); + let unsupported = format!("{}", Format::Unsupported); + + assert_eq!(toml, "TOML"); + assert_eq!(json, "JSON"); + assert_eq!(unsupported, "Unsupported format"); + } + + #[test] + fn query_result_display() { + let mut node = Node::default(); + let node_id = "nv00qmO6PDrqJheUHOONlCVpuceefS30"; + node.id = String::from(node_id); + + let none_exact = QueryResult { + node: None, + exact: true, + redirect: false, + }; + + assert!(!format!("{none_exact}").contains(node_id)); + assert!(format!("{none_exact}").contains("No Match")); + assert!(format!("{none_exact}").contains("exact")); + assert!(!format!("{none_exact}").contains("redirect")); + + let some_exact = QueryResult { + node: Some(node.clone()), + exact: true, + redirect: false, + }; + + assert!(format!("{some_exact}").contains(node_id)); + assert!(!format!("{some_exact}").contains("No Match")); + assert!(format!("{some_exact}").contains("exact")); + assert!(!format!("{some_exact}").contains("redirect")); + + let none_redirect = QueryResult { + node: None, + exact: false, + redirect: true, + }; + + assert!(!format!("{none_redirect}").contains(node_id)); + assert!(format!("{none_redirect}").contains("No Match")); + assert!(format!("{none_redirect}").contains("redirect")); + assert!(!format!("{none_redirect}").contains("exact")); + + let some_redirect = QueryResult { + node: Some(node.clone()), + exact: false, + redirect: true, + }; + + assert!(format!("{some_redirect}").contains(node_id)); + assert!(!format!("{some_redirect}").contains("No Match")); + assert!(format!("{some_redirect}").contains("redirect")); + assert!(!format!("{some_redirect}").contains("exact")); + + let some_exact_redirect = QueryResult { + node: Some(node.clone()), + exact: true, + redirect: true, + }; + + assert!(format!("{some_exact_redirect}").contains(node_id)); + assert!(!format!("{some_exact_redirect}").contains("No Match")); + assert!(format!("{some_exact_redirect}").contains("redirect")); + assert!(format!("{some_exact_redirect}").contains("exact")); + + let none_exact_redirect = QueryResult { + node: None, + exact: true, + redirect: true, + }; + + assert!(!format!("{none_exact_redirect}").contains(node_id)); + assert!(format!("{none_exact_redirect}").contains("No Match")); + assert!(format!("{none_exact_redirect}").contains("redirect")); + assert!(format!("{none_exact_redirect}").contains("exact")); + + let none = QueryResult { + node: None, + exact: false, + redirect: false, + }; + + assert!(!format!("{none}").contains(node_id)); + assert!(format!("{none}").contains("No Match")); + assert!(!format!("{none}").contains("redirect")); + assert!(!format!("{none}").contains("exact")); + + let some = QueryResult { + node: Some(node.clone()), + exact: false, + redirect: false, + }; + + assert!(format!("{some}").contains(node_id)); + assert!(!format!("{some}").contains("No Match")); + assert!(!format!("{some}").contains("redirect")); + assert!(!format!("{some}").contains("exact")); + } } #[cfg(test)] diff --git a/src/log/level.rs b/src/log/level.rs index c5054c1..55c3627 100644 --- a/src/log/level.rs +++ b/src/log/level.rs @@ -59,28 +59,26 @@ impl From for Level { impl From<&str> for Level { fn from(s: &str) -> Level { - if s == "0" || s == "SILENT" || s == "silent" { + if s == "0" || s.to_uppercase() == "SILENT" { Level::SILENT - } else if s == "1" || s == "FATAL" || s == "fatal" { + } else if s == "1" || s.to_uppercase() == "FATAL" { Level::FATAL - } else if s == "2" || s == "ERROR" || s == "error" { + } else if s == "2" || s.to_uppercase() == "ERROR" { Level::ERROR } else if s == "3" - || s == "WARN" - || s == "warn" - || s == "WARNING" - || s == "warning" + || s.to_uppercase() == "WARN" + || s.to_uppercase() == "WARNING" { Level::WARN - } else if s == "4" || s == "INFO" || s == "info" { + } else if s == "4" || s.to_uppercase() == "INFO" { Level::INFO - } else if s == "5" || s == "DEBUG" || s == "debug" { + } else if s == "5" || s.to_uppercase() == "DEBUG" { Level::DEBUG - } else if s == "6" || s == "VERBOSE" || s == "verbose" { + } else if s == "6" || s.to_uppercase() == "VERBOSE" { Level::VERBOSE - } else if s == "7" || s == "TRACE" || s == "trace" { + } else if s == "7" || s.to_uppercase() == "TRACE" { Level::TRACE - } else if s == "37" || s == "META" || s == "meta" { + } else if s == "37" || s.to_uppercase() == "META" { Level::META } else { super::ENV_DEFAULT @@ -103,3 +101,94 @@ impl std::fmt::Display for Level { write!(f, "{s}") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn u16_from_level() { + assert_eq!(u16::from(Level::SILENT), 0); + assert_eq!(u16::from(Level::FATAL), 1); + assert_eq!(u16::from(Level::ERROR), 2); + assert_eq!(u16::from(Level::WARN), 3); + assert_eq!(u16::from(Level::INFO), 4); + assert_eq!(u16::from(Level::DEBUG), 5); + assert_eq!(u16::from(Level::VERBOSE), 6); + assert_eq!(u16::from(Level::TRACE), 7); + assert_eq!(u16::from(Level::META), 37); + } + + #[test] + fn level_from_u16() { + assert_eq!(Level::from(0), Level::SILENT); + assert_eq!(Level::from(1), Level::FATAL); + assert_eq!(Level::from(2), Level::ERROR); + assert_eq!(Level::from(3), Level::WARN); + assert_eq!(Level::from(4), Level::INFO); + assert_eq!(Level::from(5), Level::DEBUG); + assert_eq!(Level::from(6), Level::VERBOSE); + assert_eq!(Level::from(7), Level::TRACE); + assert_eq!(Level::from(37), Level::META); + assert_eq!(Level::from(99), Level::WARN); + } + + #[test] + fn level_from_str() { + assert_eq!(Level::from("0"), Level::SILENT); + assert_eq!(Level::from("SILENT"), Level::SILENT); + assert_eq!(Level::from("silent"), Level::SILENT); + assert_eq!(Level::from("SiLEnT"), Level::SILENT); + + assert_eq!(Level::from("1"), Level::FATAL); + assert_eq!(Level::from("FATAL"), Level::FATAL); + assert_eq!(Level::from("fatal"), Level::FATAL); + assert_eq!(Level::from("FaTaL"), Level::FATAL); + + assert_eq!(Level::from("3"), Level::WARN); + assert_eq!(Level::from("WARN"), Level::WARN); + assert_eq!(Level::from("warn"), Level::WARN); + assert_eq!(Level::from("WaRn"), Level::WARN); + assert_eq!(Level::from("WARNING"), Level::WARN); + assert_eq!(Level::from("warning"), Level::WARN); + assert_eq!(Level::from("WaRninG"), Level::WARN); + + assert_eq!(Level::from("4"), Level::INFO); + assert_eq!(Level::from("INFO"), Level::INFO); + assert_eq!(Level::from("info"), Level::INFO); + assert_eq!(Level::from("iNFo"), Level::INFO); + + assert_eq!(Level::from("5"), Level::DEBUG); + assert_eq!(Level::from("DEBUG"), Level::DEBUG); + assert_eq!(Level::from("debug"), Level::DEBUG); + assert_eq!(Level::from("deBuG"), Level::DEBUG); + + assert_eq!(Level::from("6"), Level::VERBOSE); + assert_eq!(Level::from("VERBOSE"), Level::VERBOSE); + assert_eq!(Level::from("verbose"), Level::VERBOSE); + assert_eq!(Level::from("VerBosE"), Level::VERBOSE); + + assert_eq!(Level::from("7"), Level::TRACE); + assert_eq!(Level::from("TRACE"), Level::TRACE); + assert_eq!(Level::from("trace"), Level::TRACE); + assert_eq!(Level::from("trAcE"), Level::TRACE); + + assert_eq!(Level::from("37"), Level::META); + assert_eq!(Level::from("META"), Level::META); + assert_eq!(Level::from("meta"), Level::META); + assert_eq!(Level::from("mETa"), Level::META); + } + + #[test] + fn display_level() { + assert_eq!(format!("{}", Level::SILENT), "SILENT"); + assert_eq!(format!("{}", Level::FATAL), "FATAL"); + assert_eq!(format!("{}", Level::ERROR), "ERROR"); + assert_eq!(format!("{}", Level::WARN), "WARNING"); + assert_eq!(format!("{}", Level::INFO), "INFO"); + assert_eq!(format!("{}", Level::DEBUG), "DEBUG"); + assert_eq!(format!("{}", Level::VERBOSE), "VERBOSE"); + assert_eq!(format!("{}", Level::TRACE), "TRACE"); + assert_eq!(format!("{}", Level::META), "META"); + } +} diff --git a/src/router/handlers/fixed.rs b/src/router/handlers/fixed.rs index 3d4d4be..67a764a 100644 --- a/src/router/handlers/fixed.rs +++ b/src/router/handlers/fixed.rs @@ -168,4 +168,13 @@ mod tests { == "application/json" ); } + + #[tokio::test] + async fn not_found() { + let state = GlobalState { + graph: Graph::default(), + }; + let response = file(Path("/k/j/m".to_string()), State(state)).await; + assert!(response.status() == StatusCode::NOT_FOUND); + } } diff --git a/src/router/handlers/graph.rs b/src/router/handlers/graph.rs index bdf3235..a1912c7 100644 --- a/src/router/handlers/graph.rs +++ b/src/router/handlers/graph.rs @@ -17,18 +17,11 @@ pub async fn node( let instant = now(); let result = state.graph.find_node(&id); let found = result.node.is_some(); - let node = result - .node - .unwrap_or(Node::new(Some(format!("Could not find node ID {id}.")))); + let node = result.node.unwrap_or(Node::not_found(Some(format!( + "Could not find node ID {id}." + )))); - if !node.redirect.is_empty() { - return Redirect::permanent( - format!("/node/{}", node.redirect).as_str(), - ) - .into_response(); - } - - if found && !result.exact { + if found && (!result.exact || result.redirect) { return Redirect::permanent(format!("/node/{}", node.id).as_str()) .into_response(); } @@ -60,7 +53,7 @@ mod tests { http::{HeaderName, StatusCode}, }; - use crate::graph::Graph; + use crate::graph::{Format, Graph}; use super::*; @@ -108,4 +101,21 @@ mod tests { let response = wrap_node("docs").await; assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); } + + #[tokio::test] + async fn standalone_graph_redirect() { + let toml = "\ + [nodes.n1]\n\ + redirect = 'n2'\n\ + \n\ + [nodes.n2]\n\ + text = 'n2 text'\n\ + "; + let graph = Graph::from_serial(toml, &Format::TOML).unwrap(); + let state = GlobalState { graph }; + let response = + node(Path("n1".to_string()), axum::extract::State(state)).await; + + assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); + } } diff --git a/src/router/handlers/mime.rs b/src/router/handlers/mime.rs index 4b82089..ee87ea0 100644 --- a/src/router/handlers/mime.rs +++ b/src/router/handlers/mime.rs @@ -92,3 +92,52 @@ impl Mime { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn smoke() { + let m = Mime::guess("/home/jane/top/inner/kitty.png"); + assert_eq!(String::from(m), "image/png"); + } + + #[test] + fn all() { + let pairs = [ + ("file.txt", "text/plain"), + ("file.csv", "text/csv"), + ("file.css", "text/css"), + ("file.ttf", "font/ttf"), + ("file.otf", "font/otf"), + ("file.woff", "font/woff"), + ("file.woff2", "font/woff2"), + ("file.svg", "image/svg+xml"), + ("file.ico", "image/x-icon"), + ("file.jpeg", "image/jpeg"), + ("file.png", "image/png"), + ("file.apng", "image/apng"), + ("caddy.gif", "image/gif"), + ("file.webp", "image/webp"), + ("file.avif", "image/avif"), + ("file.toml", "application/toml"), + ("file.xml", "application/xml"), + ("file.json", "application/json"), + ("file.js", "text/javascript"), + ("file.pdf", "application/pdf"), + ("book.epub", "application/epub+zip"), + ("weird.xzx", "application/octet-stream"), + ]; + + for (file, mime) in pairs { + assert_eq!(String::from(Mime::guess(file)), mime); + } + } + + #[test] + fn unknown() { + let u = Mime::guess("x"); + assert!(matches!(u, Mime::Unknown)); + } +} diff --git a/src/syntax/content.rs b/src/syntax/content.rs index f8ccefc..0cd700b 100644 --- a/src/syntax/content.rs +++ b/src/syntax/content.rs @@ -1,3 +1,5 @@ +use std::mem::discriminant; + use parser::{Token, Lexeme}; use crate::graph::Graph; @@ -21,6 +23,26 @@ pub struct TokenOutput { pub format_tokens: Vec, } +impl TokenOutput { + pub fn only(&self, kind: &Token) -> Vec { + let filter = |tokens: &[Token], k: &Token| -> Vec { + tokens + .iter() + .filter(|&t| discriminant(t) == discriminant(k)) + .cloned() + .collect::>() + }; + + let filtered_tokens = filter(&self.tokens, kind); + let filtered_format_tokens = filter(&self.format_tokens, kind); + + [filtered_tokens, filtered_format_tokens] + .into_iter() + .flatten() + .collect::>() + } +} + pub fn parse(text: &str, graph: &Graph) -> String { parser::read(text, graph) } @@ -28,3 +50,21 @@ pub fn parse(text: &str, graph: &Graph) -> String { pub fn rich_parse(text: &str, graph: &Graph) -> TokenOutput { parser::rich_read(text, graph) } + +#[cfg(test)] +mod tests { + use crate::syntax::content::parser::token::{Bold, Oblique}; + + use super::*; + + #[test] + fn only() { + let graph = Graph::default(); + let output = rich_parse("*four* *bold* and _two_ italic", &graph); + let bold_tokens = output.only(&Token::Bold(Bold::new(true))); + let italic_tokens = output.only(&Token::Oblique(Oblique::new(true))); + println!("{bold_tokens:?}"); + assert_eq!(bold_tokens.len(), 4); + assert_eq!(italic_tokens.len(), 2); + } +} diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index 85ddee5..e8709fa 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -1,4 +1,4 @@ -use crate::syntax::content::Parseable as _; +use crate::syntax::content::Parseable; pub mod anchor; pub mod bold; diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs index 94aac28..0eb2570 100644 --- a/src/syntax/content/parser/token/anchor.rs +++ b/src/syntax/content/parser/token/anchor.rs @@ -15,29 +15,6 @@ pub struct Anchor { } impl Anchor { - pub fn new( - text: &str, - destination: &str, - node: Option, - node_id: Option, - leading: bool, - external: bool, - balanced: bool, - ) -> Anchor { - let mut anchor = Anchor { - text: text.to_owned(), - destination: Some(String::from(destination)), - node, - node_id, - leading, - external, - balanced, - }; - - anchor.route(); - anchor - } - pub fn text(&self) -> String { self.text.clone() } @@ -287,4 +264,18 @@ mod tests { anchor.route(); // set_destination also called this assert!(anchor.destination().is_none()); } + + #[test] + fn set_node_id() { + let payload = "kxBDJ0EoDVaygxpZ8NgNdQrUIBsGimTs"; + let mut anchor = Anchor::default(); + anchor.set_node_id(payload); + assert_eq!(anchor.node_id.unwrap(), payload); + } + + #[test] + fn display_no_destination() { + let anchor = Anchor::default(); + assert_eq!(format!("{anchor}"), "Anchor -> "); + } } diff --git a/src/syntax/content/parser/token/bold.rs b/src/syntax/content/parser/token/bold.rs index e2a7efc..0252a67 100644 --- a/src/syntax/content/parser/token/bold.rs +++ b/src/syntax/content/parser/token/bold.rs @@ -76,4 +76,10 @@ mod tests { "Tk:Bold [closed]" ); } + + #[test] + fn flatten() { + let bold = Bold::new(false); + assert_eq!(bold.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/checkbox.rs b/src/syntax/content/parser/token/checkbox.rs index 656f419..777e499 100644 --- a/src/syntax/content/parser/token/checkbox.rs +++ b/src/syntax/content/parser/token/checkbox.rs @@ -75,4 +75,10 @@ mod tests { "Tk:CheckBox [empty]" ); } + + #[test] + fn flatten() { + let checkbox = CheckBox::new(false); + assert_eq!(checkbox.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/header.rs b/src/syntax/content/parser/token/header.rs index f238f73..e18603c 100644 --- a/src/syntax/content/parser/token/header.rs +++ b/src/syntax/content/parser/token/header.rs @@ -300,4 +300,30 @@ mod tests { "Tk:Header [closed L2]" ); } + + #[test] + fn display_unknown_open_state() { + let header = Header { + open: None, + dom_id: None, + level: Level::One, + }; + + assert_eq!(format!("{header}"), "Header [unknown L1]"); + } + + #[test] + fn display_dom_id() { + let payload = "WIV0h1wCeY7Pp3FkjgIftJHX1I6YvnSc"; + let header = Header { + open: None, + dom_id: Some(payload.to_string()), + level: Level::One, + }; + + assert_eq!( + format!("{header}"), + format!("Header [unknown L1 DOM ID {payload}]") + ); + } } diff --git a/src/syntax/content/parser/token/item.rs b/src/syntax/content/parser/token/item.rs index 02b8431..7652898 100644 --- a/src/syntax/content/parser/token/item.rs +++ b/src/syntax/content/parser/token/item.rs @@ -89,4 +89,10 @@ mod tests { "Tk:Item [] dRMy4" ); } + + #[test] + fn flatten() { + let item = Item::new("", None); + assert_eq!(item.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs index 721d226..ca73804 100644 --- a/src/syntax/content/parser/token/list.rs +++ b/src/syntax/content/parser/token/list.rs @@ -189,4 +189,23 @@ mod tests { let lexeme = Lexeme::new("SL6PX", "6xsNB", "oeAHa"); List::lex(&lexeme); } + + #[test] + fn ordered_list() { + let mut list = List::new(true); + list.items = vec![ + Item::new("a", Some(0)), + Item::new("b", Some(0)), + Item::new("c", Some(0)), + ]; + + assert_eq!( + list.render(), + "\n
    \n\ +
  1. a
  2. \n\ +
  3. b
  4. \n\ +
  5. c
  6. \n\ +
\n\n" + ); + } } diff --git a/src/syntax/content/parser/token/oblique.rs b/src/syntax/content/parser/token/oblique.rs index 320626b..0d69468 100644 --- a/src/syntax/content/parser/token/oblique.rs +++ b/src/syntax/content/parser/token/oblique.rs @@ -79,4 +79,10 @@ mod tests { "Tk:Oblique [closed]" ); } + + #[test] + fn flatten() { + let oblique = Oblique::new(false); + assert_eq!(oblique.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/preformat.rs b/src/syntax/content/parser/token/preformat.rs index a55995b..89e71dc 100644 --- a/src/syntax/content/parser/token/preformat.rs +++ b/src/syntax/content/parser/token/preformat.rs @@ -99,4 +99,10 @@ mod tests { "Tk:PreFormat [unknown]" ); } + + #[test] + fn flatten() { + let preformat = PreFormat::new(false); + assert_eq!(preformat.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/strike.rs b/src/syntax/content/parser/token/strike.rs index c1a925a..14b4407 100644 --- a/src/syntax/content/parser/token/strike.rs +++ b/src/syntax/content/parser/token/strike.rs @@ -76,4 +76,10 @@ mod tests { "Tk:Strike [closed]" ); } + + #[test] + fn flatten() { + let strike = Strike::new(false); + assert_eq!(strike.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/underline.rs b/src/syntax/content/parser/token/underline.rs index a86e4b4..2da61b7 100644 --- a/src/syntax/content/parser/token/underline.rs +++ b/src/syntax/content/parser/token/underline.rs @@ -79,4 +79,10 @@ mod tests { "Tk:Underline [closed]" ); } + + #[test] + fn flatten() { + let underline = Underline::new(false); + assert_eq!(underline.flatten(), ""); + } } diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 5859c52..1db505b 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -320,6 +320,7 @@ td, th { summary { margin-bottom: 15px; + cursor: pointer; } hr { From 7e9f3c3afbec5214ba12ef2d9c413b0211504865 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:53:43 -0300 Subject: [PATCH 007/108] Minor refactorings, doc comments --- Cargo.toml | 1 - src/graph/meta.rs | 62 ++++++++++++++++++------------ src/syntax/content/parser/token.rs | 2 +- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6b60a1b..a002498 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,6 @@ doc_comment_double_space_linebreaks = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" empty_enum = "warn" -enum_glob_use = "warn" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" diff --git a/src/graph/meta.rs b/src/graph/meta.rs index eb7da88..56664e8 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -145,11 +145,23 @@ impl std::fmt::Display for Version { } impl Version { + /// Parses the compile-time version into a Version struct. + /// + /// # Errors + /// This function is a thin wrapper around `Meta::from_text` and will + /// return its errors without change. pub fn from_compilation() -> Result { - Version::from_str(env!("CARGO_PKG_VERSION")) + Version::from_text(env!("CARGO_PKG_VERSION")) } - pub fn from_str(version: &str) -> Result { + /// Parses a string into a Version struct + /// + /// It is expected for the version string to contain exactly three + /// dot-separated numeric values with an optional leading `v` character. + /// + /// # Errors + /// Will error if the version string is malformed. + pub fn from_text(version: &str) -> Result { use VersionErrorCause::*; let triple: Vec = @@ -384,7 +396,7 @@ mod tests { #[test] fn version_from_str() { let payload = "3.9.74"; - let version_result = Version::from_str(payload); + let version_result = Version::from_text(payload); println!("{version_result:#?}"); assert_eq!(format!("{}", version_result.unwrap()), payload); @@ -392,30 +404,30 @@ mod tests { #[test] fn missing_major() { - let error = Version::from_str("").unwrap_err(); + let error = Version::from_text("").unwrap_err(); println!("{error:#?}"); assert!(matches!(error.cause, VersionErrorCause::MissingMajor)); } #[test] fn missing_minor() { - let error = Version::from_str("3").unwrap_err(); + let error = Version::from_text("3").unwrap_err(); println!("{error:#?}"); assert!(matches!(error.cause, VersionErrorCause::MissingMinor)); } #[test] fn missing_patch() { - let error = Version::from_str("3.6").unwrap_err(); + let error = Version::from_text("3.6").unwrap_err(); assert!(matches!(error.cause, VersionErrorCause::MissingPatch)); } #[test] fn malformed_patch() { - let error = Version::from_str("3.6.x").unwrap_err(); + let error = Version::from_text("3.6.x").unwrap_err(); assert!(matches!(error.cause, VersionErrorCause::FailedPatchParse)); - let error_empty = Version::from_str("3.6.").unwrap_err(); + let error_empty = Version::from_text("3.6.").unwrap_err(); assert!(matches!( error_empty.cause, VersionErrorCause::FailedPatchParse @@ -424,28 +436,28 @@ mod tests { #[test] fn malformed_minor() { - let error = Version::from_str("3.x.0").unwrap_err(); + let error = Version::from_text("3.x.0").unwrap_err(); assert!(matches!(error.cause, VersionErrorCause::FailedMinorParse)); - let error_bad_patch = Version::from_str("3.x.z").unwrap_err(); + let error_bad_patch = Version::from_text("3.x.z").unwrap_err(); assert!(matches!( error_bad_patch.cause, VersionErrorCause::FailedMinorParse )); - let error_empty_patch = Version::from_str("3.x.").unwrap_err(); + let error_empty_patch = Version::from_text("3.x.").unwrap_err(); assert!(matches!( error_empty_patch.cause, VersionErrorCause::FailedMinorParse )); - let error_patchless = Version::from_str("3.x").unwrap_err(); + let error_patchless = Version::from_text("3.x").unwrap_err(); assert!(matches!( error_patchless.cause, VersionErrorCause::FailedMinorParse )); - let error_empty = Version::from_str("3.").unwrap_err(); + let error_empty = Version::from_text("3.").unwrap_err(); assert!(matches!( error_empty.cause, VersionErrorCause::FailedMinorParse @@ -454,67 +466,67 @@ mod tests { #[test] fn malformed_major() { - let error = Version::from_str("x.6.0").unwrap_err(); + let error = Version::from_text("x.6.0").unwrap_err(); assert!(matches!(error.cause, VersionErrorCause::FailedMajorParse)); - let error_bad_patch = Version::from_str("x.y.z").unwrap_err(); + let error_bad_patch = Version::from_text("x.y.z").unwrap_err(); assert!(matches!( error_bad_patch.cause, VersionErrorCause::FailedMajorParse )); - let error_empty_patch = Version::from_str("x.6.").unwrap_err(); + let error_empty_patch = Version::from_text("x.6.").unwrap_err(); assert!(matches!( error_empty_patch.cause, VersionErrorCause::FailedMajorParse )); - let error_patchless = Version::from_str("x.6").unwrap_err(); + let error_patchless = Version::from_text("x.6").unwrap_err(); assert!(matches!( error_patchless.cause, VersionErrorCause::FailedMajorParse )); - let error_bad_minor = Version::from_str("x.y").unwrap_err(); + let error_bad_minor = Version::from_text("x.y").unwrap_err(); assert!(matches!( error_bad_minor.cause, VersionErrorCause::FailedMajorParse )); - let error_empty_minor = Version::from_str("x.").unwrap_err(); + let error_empty_minor = Version::from_text("x.").unwrap_err(); assert!(matches!( error_empty_minor.cause, VersionErrorCause::FailedMajorParse )); - let error_minorless = Version::from_str("x").unwrap_err(); + let error_minorless = Version::from_text("x").unwrap_err(); assert!(matches!( error_minorless.cause, VersionErrorCause::FailedMajorParse )); - let error_empty = Version::from_str("").unwrap_err(); + let error_empty = Version::from_text("").unwrap_err(); assert!(matches!(error_empty.cause, VersionErrorCause::MissingMajor)); } #[test] fn version_validation() { assert!(["3.1.4.", "3.1.4.1"].iter().all(|s| matches!( - Version::from_str(s).unwrap_err().cause, + Version::from_text(s).unwrap_err().cause, VersionErrorCause::FailedValidation ))); } #[test] fn leading_v() { - let version = Version::from_str("v3.1.4").unwrap(); + let version = Version::from_text("v3.1.4").unwrap(); assert_eq!(format!("{version}"), "3.1.4"); } #[test] fn display_version_error_cause() { fn assert(version: &str, message: &str) { - let error = Version::from_str(version).unwrap_err(); + let error = Version::from_text(version).unwrap_err(); assert_eq!(format!("{error}"), message); } @@ -549,7 +561,7 @@ mod tests { invalid digit found in string", ); - let validation_error = Version::from_str("3.1.4.1..").unwrap_err(); + let validation_error = Version::from_text("3.1.4.1..").unwrap_err(); println!("{validation_error}"); assert!(matches!( diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index e8709fa..85ddee5 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -1,4 +1,4 @@ -use crate::syntax::content::Parseable; +use crate::syntax::content::Parseable as _; pub mod anchor; pub mod bold; From c5174824ec3355a400b815caae7e93b75b71074f Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 02:08:42 -0300 Subject: [PATCH 008/108] Fix mismatched font sizes across prose faces --- static/public/assets/fonts/fonts.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/public/assets/fonts/fonts.css b/static/public/assets/fonts/fonts.css index 65bfaea..1acbd56 100644 --- a/static/public/assets/fonts/fonts.css +++ b/static/public/assets/fonts/fonts.css @@ -45,6 +45,7 @@ src: url("reforma/Reforma1969-Gris.woff2") format("woff2"); font-family: "prose"; font-weight: bold; + size-adjust: 120%; display: swap; } @@ -53,6 +54,7 @@ src: url("prose-italic"); font-family: "prose"; font-style: italic; + size-adjust: 120%; display: swap; } @@ -61,5 +63,6 @@ font-family: "prose"; font-weight: bold; font-style: italic; + size-adjust: 120%; display: swap; } From 5156161650cc743bda1e6da1c8340337ac03d845 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 02:29:14 -0300 Subject: [PATCH 009/108] Sketch out a possible table syntax in the roadmap --- static/graph.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/graph.toml b/static/graph.toml index 2ac5285..9ac9c6b 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -584,6 +584,11 @@ text = """ - [ ] Invert where redirects are set - [x] Formatting - [ ] Blockquotes + - [ ] Tables + - `%` block + - newline for rows + - indented, space-surrounded `!` wrap for headers + - indented, space-surrounded `|` wrap for cells - [x] Nested formatting - [x] Headers - [x] Preformatted blocks From 21a710658d308e337188c0ce53e3237c74d09166 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 21 Jan 2026 16:52:54 -0300 Subject: [PATCH 010/108] Analyze necessity and effects of checked arithmetic --- src/graph.rs | 12 ++++++++++ src/syntax/content/parser/context/list.rs | 3 +++ src/syntax/content/parser/token/list.rs | 29 +++++++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index e94c936..914914e 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -207,6 +207,10 @@ impl Graph { } } + // Modulates nodes that have been deserialized and are still unprocessed. + // + // Performs checked arithmetic to the following effect: + // - Stats will saturate at u32::MAX (increment_detached calls) fn modulate_nodes(&mut self) { let in_nodes = self.nodes.clone(); @@ -306,6 +310,10 @@ impl Graph { } } + // Modulates edges that have been deserialized and are still unprocessed. + // + // This function performs checked arithmetic to the following effect: + // - Stats will saturate at u32::MAX fn modulate_edges(&mut self) { let graph = self.clone(); let iterator = self.nodes.iter_mut(); @@ -364,6 +372,10 @@ impl Graph { } } + // Increments detached node statistics for the given node ID + // + // This function performs checked arithmetic to the following effect: + // - Stats will saturate at u32::MAX fn increment_detached(&mut self, node_id: &str) { self.stats .detached diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index 415c0ea..45b5099 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -13,6 +13,9 @@ use crate::{ /// A return of `true` will trigger a continue in the outer parser, /// skipping any further parsing of the current lexeme. /// +/// Performs checked arithmetic to the following effect: +/// - The indent of list items will saturate at `u8::MAX` (255) spaces. +/// /// # Panics /// This parser can handle only the List context, and will panic if passed an /// unrelated context since it has no knowledge on how to handle them. diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs index ca73804..82deb46 100644 --- a/src/syntax/content/parser/token/list.rs +++ b/src/syntax/content/parser/token/list.rs @@ -1,4 +1,5 @@ use crate::{ + prelude::*, syntax::content::{ Parseable, parser::{Lexeme, token::Item}, @@ -20,6 +21,14 @@ impl Parseable for List { panic!("Attempt to lex a List directly from a lexeme") } + /// Renders the list to the equivalent HTML representation. + /// + /// Performs checked arithmetic to the following effects: + /// - Strict division is performed but related panics are unreachable given + /// the guarantees described in `List::scale_indent` + /// - Saturates subtractions from indent levels at zero. This is not + /// unreachable, but a difference of zero is a no-op considering it + /// would cause an iteration of zero times (over an empty range). fn render(&self) -> String { let tag = if self.ordered { "ol" } else { "ul" }; let mut output = String::new(); @@ -68,6 +77,18 @@ impl List { } } + /// Calculates the scale to normalize indents. + /// + /// For example, if two contiguous items have differing indents of 2 and 4, + /// the indent scale is 2 and they can be normalized as having indents of + /// 1 and 2 respectively. + /// + /// Performs checked arithmetic to the following effects: + /// - The subtraction of outer from inner saturates at 0 due to u8 being + /// unsigned, but such a case is unreachable given the outer condition + /// that guards this subtraction + /// - Will not return zero even if it is the calculated width, instead + /// logging the event and returning 1 instead fn scale_indent(&self) -> u8 { let width = self .items @@ -79,8 +100,12 @@ impl List { }) .unwrap_or(1); - assert!(width != 0, "Width of zero can't be a divisor"); - width + if width == 0 { + log!("Scale indent of 0 can't be a divisor: returning 1 instead"); + 1 + } else { + width + } } } From 95411ef605ad8d24c995a2eab0c99d008bf358e0 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 21 Jan 2026 21:02:16 -0300 Subject: [PATCH 011/108] Make doc comments in graph.rs more consistent --- src/graph.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 914914e..709859f 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -207,10 +207,10 @@ impl Graph { } } - // Modulates nodes that have been deserialized and are still unprocessed. - // - // Performs checked arithmetic to the following effect: - // - Stats will saturate at u32::MAX (increment_detached calls) + /// Modulates nodes that have been deserialized and are still unprocessed. + /// + /// Performs checked arithmetic to the following effect: + /// - Stats will saturate at `u32::MAX` (`increment_detached` calls) fn modulate_nodes(&mut self) { let in_nodes = self.nodes.clone(); @@ -310,10 +310,10 @@ impl Graph { } } - // Modulates edges that have been deserialized and are still unprocessed. - // - // This function performs checked arithmetic to the following effect: - // - Stats will saturate at u32::MAX + /// Modulates edges that have been deserialized and are still unprocessed. + /// + /// Performs checked arithmetic to the following effect: + /// - Stats will saturate at `u32::MAX` fn modulate_edges(&mut self) { let graph = self.clone(); let iterator = self.nodes.iter_mut(); @@ -372,10 +372,10 @@ impl Graph { } } - // Increments detached node statistics for the given node ID - // - // This function performs checked arithmetic to the following effect: - // - Stats will saturate at u32::MAX + /// Increments detached node statistics for the given node ID + /// + /// Performs checked arithmetic to the following effect: + /// - Stats will saturate at `u32::MAX` fn increment_detached(&mut self, node_id: &str) { self.stats .detached From 3ea6c539200e0a896c8c3ff6b91799975871080e Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Feb 2026 14:17:20 -0300 Subject: [PATCH 012/108] Adjust visited anchor color, header sizes and wrapping --- static/public/assets/fonts/fonts.css | 1 - static/public/assets/style.css | 36 +++++++++++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/static/public/assets/fonts/fonts.css b/static/public/assets/fonts/fonts.css index 1acbd56..442c12d 100644 --- a/static/public/assets/fonts/fonts.css +++ b/static/public/assets/fonts/fonts.css @@ -51,7 +51,6 @@ @font-face { src: url("reforma/Reforma1969-BlancaItalica.woff2") format("woff2"); - src: url("prose-italic"); font-family: "prose"; font-style: italic; size-adjust: 120%; diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 1db505b..328001a 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -72,9 +72,6 @@ a { text-decoration: underline dotted #138e8e; text-decoration-thickness: 1.5px; text-underline-offset: 3px; -} - -a.attached { transition: 1500ms; } @@ -82,8 +79,8 @@ a.attached:hover, #nav-main a:hover { color: #117c7c; - text-decoration-color: #10afaf; text-shadow: 0px 0px 22px #10afaf; + transition: 1500ms; } a.detached { @@ -101,6 +98,7 @@ a.external { a.external:hover { color: #0393b2; text-decoration-color: #1ed4f1; + transition: 1500ms; } a:visited, @@ -108,6 +106,8 @@ a.detached:visited, a.external:visited { text-decoration-color: #999; + color: #7e20cf; + transition: 1500ms; } footer div a { @@ -155,6 +155,7 @@ span.hidden-label { h1 { font-family: title, serif; + line-height: 1; } h2, h3, h4, h5, h6 { @@ -168,6 +169,7 @@ main h2 { h1, h2, h3, h4, h5, h6 { font-weight: 1; margin: 10px 0; + overflow-wrap: break-word; } h1.node-title { @@ -184,11 +186,11 @@ h3.connections-title { } h1 { font-size: calc(var(--base-font-size) * 3.6); } -h2 { font-size: calc(var(--base-font-size) * 3.4); } -h3 { font-size: calc(var(--base-font-size) * 3.0); } -h4 { font-size: calc(var(--base-font-size) * 2.6); } -h5 { font-size: calc(var(--base-font-size) * 2.2); } -h6 { font-size: calc(var(--base-font-size) * 1.8); } +h2 { font-size: calc(var(--base-font-size) * 2.8); } +h3 { font-size: calc(var(--base-font-size) * 2.4); } +h4 { font-size: calc(var(--base-font-size) * 2.0); } +h5 { font-size: calc(var(--base-font-size) * 1.6); } +h6 { font-size: calc(var(--base-font-size) * 1.2); } footer div { margin: 20px 0; @@ -298,7 +300,6 @@ div#error-poem { } em, i { - font-style: oblique 20deg; font-weight: 300; padding-right: 2px; } @@ -345,28 +346,32 @@ footer hr { a { color: #1dd7d7; text-decoration-color: #159b9b; + transition: 1500ms; } a.attached:hover, #nav-main a:hover { color: #00ffff; - text-decoration-color: #66ffff; + transition: 1500ms; } a.external { color: #2fbae4; text-decoration-color: #46c1e7; + transition: 1500ms; } a.external:hover { color: #74e5ff; text-decoration-color: #aeffff; + transition: 1500ms; } a.detached { color: #acacac; text-decoration-color: #777; + transition: 1500ms; } span.id-label { @@ -381,6 +386,15 @@ footer hr { } + a:visited, + a.detached:visited, + a.external:visited + { + text-decoration-color: #999; + color: #a3a5ff; + transition: 1500ms; + } + input[type="text"], input[type="submit"], select, From aa41e33ced395c0533fb66ae289758f63e6c54c7 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Feb 2026 14:52:16 -0300 Subject: [PATCH 013/108] Implement verse token, scaffold quote token --- src/syntax/content/parser/context.rs | 10 +- src/syntax/content/parser/context/block.rs | 45 +++++- src/syntax/content/parser/context/list.rs | 5 +- src/syntax/content/parser/token.rs | 14 +- src/syntax/content/parser/token/linebreak.rs | 4 +- src/syntax/content/parser/token/quote.rs | 58 +++++++ src/syntax/content/parser/token/verse.rs | 72 +++++++++ static/graph.toml | 151 +++++++++++++++---- static/public/assets/style.css | 11 ++ 9 files changed, 325 insertions(+), 45 deletions(-) create mode 100644 src/syntax/content/parser/token/quote.rs create mode 100644 src/syntax/content/parser/token/verse.rs diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index e536bd2..7174ecf 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -1,6 +1,6 @@ use crate::syntax::content::parser::{ State, Token, - token::{Header, Paragraph, PreFormat}, + token::{Header, Paragraph, PreFormat, Quote, Verse}, }; pub mod block; @@ -20,6 +20,8 @@ pub enum Block { Header(u8), // level List, PreFormat, + Quote, + Verse, None, } @@ -46,6 +48,12 @@ pub fn close(state: &State, tokens: &mut Vec) { Block::Header(level) => { tokens.push(Token::Header(Header::from_u8(level, false, None))); }, + Block::Quote => { + tokens.push(Token::Quote(Quote::new(false))); + }, + Block::Verse => { + tokens.push(Token::Verse(Verse::new(false))); + }, Block::None => (), } } diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index cbf51b2..64b080d 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -1,15 +1,18 @@ use std::{iter::Peekable, slice::Iter}; use crate::{ + graph::Graph, prelude::*, syntax::content::{ Parseable as _, parser::{ - Block, Token, Lexeme, State, - token::{Header, List, Literal, Paragraph, PreFormat}, + Block, Lexeme, State, Token, + token::{ + Header, List, LineBreak, Literal, Paragraph, PreFormat, Quote, + Verse, + }, }, }, - graph::Graph, }; pub fn parse( @@ -44,6 +47,18 @@ pub fn parse( return super::list::parse( lexeme, state, tokens, iterator, graph, ); + } else if Quote::probe(lexeme) { + log!(VERBOSE, "Block Context: None -> Quote on {lexeme}"); + state.context.block = Block::Quote; + tokens.push(Token::Quote(Quote::new(true))); + return true; + } else if Verse::probe(lexeme) { + log!(VERBOSE, "Block Context: None -> Verse on {lexeme}"); + state.context.block = Block::Verse; + tokens.push(Token::Verse(Verse::new(true))); + iterator.next(); + iterator.next(); + return true; } else if Paragraph::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Paragraph on {lexeme}"); state.context.block = Block::Paragraph; @@ -77,6 +92,30 @@ pub fn parse( Block::List => { return super::list::parse(lexeme, state, tokens, iterator, graph); }, + Block::Quote => { + if lexeme.match_char_sequence('\n', '>') { + tokens.push(Token::LineBreak(LineBreak::default())); + iterator.next(); + return true; + } else if Quote::probe_end(lexeme) { + tokens.push(Token::Quote(Quote::new(false))); + log!(VERBOSE, "Block Context: Quote -> None on {lexeme}"); + state.context.block = Block::None; + } + }, + Block::Verse => { + if Verse::probe_end(lexeme) { + tokens.push(Token::Verse(Verse::new(false))); + log!(VERBOSE, "Block Context: Verse -> None on {lexeme}"); + state.context.block = Block::None; + iterator.next(); + iterator.next(); + return true; + } else if lexeme.match_char('\n') { + tokens.push(Token::LineBreak(LineBreak::default())); + return true; + } + }, } false } diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index 45b5099..bec908d 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -77,10 +77,7 @@ pub fn parse( item_candidate.text.push_str(&lexeme.text()); } }, - Block::None - | Block::Paragraph - | Block::Header(_) - | Block::PreFormat => { + _ => { panic!("List context parser called to handle non-list context") }, } diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index 85ddee5..a64f48b 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -1,4 +1,4 @@ -use crate::syntax::content::Parseable as _; +use crate::syntax::content::{Parseable as _}; pub mod anchor; pub mod bold; @@ -12,14 +12,16 @@ pub mod literal; pub mod oblique; pub mod paragraph; pub mod preformat; +pub mod quote; pub mod strike; pub mod underline; +pub mod verse; pub use { anchor::Anchor, bold::Bold, checkbox::CheckBox, code::Code, header::Header, item::Item, linebreak::LineBreak, list::List, literal::Literal, oblique::Oblique, paragraph::Paragraph, preformat::PreFormat, - strike::Strike, underline::Underline, + strike::Strike, underline::Underline, quote::Quote, verse::Verse, }; #[derive(Debug, Eq, PartialEq, Clone)] @@ -37,7 +39,9 @@ pub enum Token { Oblique(Oblique), Paragraph(Paragraph), PreFormat(PreFormat), + Quote(Quote), Underline(Underline), + Verse(Verse), } impl Token { @@ -56,7 +60,9 @@ impl Token { Token::Oblique(d) => d.render(), Token::Paragraph(d) => d.render(), Token::PreFormat(d) => d.render(), + Token::Quote(d) => d.render(), Token::Underline(d) => d.render(), + Token::Verse(d) => d.render(), } } @@ -75,7 +81,9 @@ impl Token { Token::Oblique(d) => d.flatten(), Token::Paragraph(d) => d.flatten(), Token::PreFormat(d) => d.flatten(), + Token::Quote(d) => d.flatten(), Token::Underline(d) => d.flatten(), + Token::Verse(d) => d.flatten(), } } } @@ -96,7 +104,9 @@ impl std::fmt::Display for Token { Token::Oblique(d) => format!("{d}"), Token::Paragraph(d) => format!("{d}"), Token::PreFormat(d) => format!("{d}"), + Token::Quote(d) => format!("{d}"), Token::Underline(d) => format!("{d}"), + Token::Verse(d) => format!("{d}"), }; write!(f, "Tk:{data}") diff --git a/src/syntax/content/parser/token/linebreak.rs b/src/syntax/content/parser/token/linebreak.rs index 9026b7c..6996815 100644 --- a/src/syntax/content/parser/token/linebreak.rs +++ b/src/syntax/content/parser/token/linebreak.rs @@ -7,7 +7,7 @@ pub struct LineBreak {} impl Parseable for LineBreak { fn probe(lexeme: &Lexeme) -> bool { - lexeme.text() == "\n" && !lexeme.last() + lexeme.match_char('<') && lexeme.match_next_char('\n') } fn lex(_lexeme: &Lexeme) -> LineBreak { @@ -15,7 +15,7 @@ impl Parseable for LineBreak { } fn render(&self) -> String { - "\n".to_owned() + String::from("
") } fn flatten(&self) -> String { diff --git a/src/syntax/content/parser/token/quote.rs b/src/syntax/content/parser/token/quote.rs new file mode 100644 index 0000000..e9bdf49 --- /dev/null +++ b/src/syntax/content/parser/token/quote.rs @@ -0,0 +1,58 @@ +use crate::syntax::content::{Parseable, parser::Lexeme}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Quote { + open: Option, +} + +impl Quote { + pub fn new(open: bool) -> Quote { + Quote { open: Some(open) } + } + + pub fn probe_end(lexeme: &Lexeme) -> bool { + lexeme.match_char_sequence('\n', '\n') + } +} + +impl Parseable for Quote { + fn probe(lexeme: &Lexeme) -> bool { + lexeme.match_char('>') && lexeme.match_next_char(' ') + } + + fn lex(_lexeme: &Lexeme) -> Quote { + Quote { open: None } + } + + fn render(&self) -> String { + if let Some(open) = self.open { + if open { + "
".to_owned() + } else { + "
".to_owned() + } + } else { + panic!("Attempt to render a quote tag while open state is unknown") + } + } + + fn flatten(&self) -> String { + String::default() + } +} + +impl std::fmt::Display for Quote { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let display_open_state = match self.open { + Some(open_state) => { + if open_state { + "open" + } else { + "closed" + } + }, + None => "unknown", + }; + write!(f, "Quote [{display_open_state}]") + } +} diff --git a/src/syntax/content/parser/token/verse.rs b/src/syntax/content/parser/token/verse.rs new file mode 100644 index 0000000..6dbeb9b --- /dev/null +++ b/src/syntax/content/parser/token/verse.rs @@ -0,0 +1,72 @@ +use crate::syntax::content::{Parseable, parser::Lexeme}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Verse { + open: Option, + citation: Option, +} + +impl Verse { + pub fn new(open: bool) -> Verse { + Verse { + open: Some(open), + citation: None, + } + } + + pub fn probe_end(lexeme: &Lexeme) -> bool { + lexeme.match_char_triple('\n', '&', '\n') + } +} + +impl Parseable for Verse { + fn probe(lexeme: &Lexeme) -> bool { + lexeme.match_char_triple('\n', '&', '\n') + } + + fn lex(_lexeme: &Lexeme) -> Verse { + Verse { + open: None, + citation: None, + } + } + + fn render(&self) -> String { + if let Some(open) = self.open { + if open { + concat!("\n", r#"

"#).to_string() + } else { + "\n

\n".to_owned() + } + } else { + panic!("Attempt to render a verse tag while open state is unknown") + } + } + + fn flatten(&self) -> String { + String::default() + } +} + +impl std::fmt::Display for Verse { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let display_open_state = match self.open { + Some(open_state) => { + if open_state { + "open" + } else { + "closed" + } + }, + None => "unknown", + }; + + let citation = if self.citation.is_some() { + " cited" + } else { + "" + }; + + write!(f, "Verse [{display_open_state}{citation}]") + } +} diff --git a/static/graph.toml b/static/graph.toml index 9ac9c6b..cb33cf5 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -276,9 +276,33 @@ Supported formatting syntax includes: To apply these, you can wrap a word in the formatting operators, so for instance `*this*` will be rendered as *this* and `~~this~~` as ~~this~~. -## Paragraphs +## Checkboxes -A block of lines not separated by an empty line is always joined together. This means if you write: +You can use `[ ]` and `[x]` to render checkboxes: + +` +- [ ] not done +- [x] done +` + +## Blocks + +A block is any group of lines separated by empty lines: + +` +block A +still block A +still block A + +block B starts here + +block C starts here +still block C +` + +By default, a block not starting with any special syntax is a paragraph, such as this very line you are reading. + +Some blocks will join the lines together, meaning even if you write: ` a @@ -286,11 +310,81 @@ b c ` -You still get "a b c" as a result. +You still get "a b c" as a result. This is the case for paragraphs and blockquotes, but not for lists, verse blocks and preformatted text. -The exception to this are lists, which are explained below and must have their lines grouped together. +This is useful when editing your text, allowing you to break some thoughts and special syntax without losing control over where your paragraph ends, particularly when handling huge paragraphs. -## Lists +If you want to force lines to break, you can use a `<` character at the end of a line: + +` +a < +b < +c < +` + +Which renders as: + +a < +b < +c < + +While useful to break a few lines on demand, if you have a large block of lines you want to break this can become cumbersome. That's where verse blocks are useful. + +### Verse + +Verse blocks are delimited by a `&` character at their first and last line and are useful to avoid precisely this line-joining behavior of most blocks. They will break all lines without need for a trailing `<` character: + +` +& + these lines + break just fine + once they are over +& +` + +This will be rendered as: + +& + these lines + break just fine + once they are over +& + +### Quotes + +A block of lines starting with a `>` character create a quote: + +` +> this is a quote +` + +Quote blocks have two forms. If you prepend all blocks with a `>`, line breaks will be preserved, not collapsing the whole quote into a single line: + +> a quote where all lines start with > BR +> still inside BR +> still inside BR +> +> above was a line with just a > plus a break BR +> still inside BR +> next is an empty line BR + +If you would like the quote to be collapsed into a single line instead, you can leave just the first `>` and continue until the next empty line: + +> this quote starts here +and continues here +until an empty line is found + +You can still use `<` characters to force line breaks in this case. + +#### Citation + +To add a citation to your qutoe block, you can add lines starting with two `-` characters: + +> a quote with _nested_ formatting *syntax and in* particular an anchor|Roadmap BR +> the quote continuation as it goes on and on +-- quote by |Person Johnson|https://personjohnson.com + +### Lists A block of lines starting with a `-` character will be rendered as an unordered list: @@ -308,15 +402,28 @@ Lines starting with a `+` character will create numbered lists instead: + san ` -## Checkboxes +## Rendering unformatted text -You can use `[ ]` and `[x]` to render checkboxes: +The backtick character `\\`` can be used to render unformatted blocks and inline text: ` -- [ ] not done -- [x] done +The asterisk `*` is special in en markup syntax. ` +Using the syntax above, the asterisk won't be interpreted as the start of bold formatting and instead will be shown like this: `*`. + +This is useful for code but also for rendering characters with special meaning you wish to mention literally. + +Backticks on their own line will start and close a block of unformatted text such as the ones being used throughout this documentation to show code: + +` +\\` +everything in here will be rendered without formatting +\\` +` + +Finally, you can precede any character with a `\\\\` to fully _escape_ that character from being interpreted. Because |TOML| also treats backslashes specially, you'll likely need to use double slashes, as in `\\\\\\\\`, unless you wrap your TOML strings in single quotes. See |Escaping| for more details and examples. + ## Raw HTML If you need some more advanced feature that is not supported directly by en's markup snytax, you can always just write plain HTML and it will be passed along. For example, you could render a table: @@ -341,29 +448,7 @@ Which will render to: Notice that, as shown in this example, you can mix en syntax and HTML. You might want to add a space between your HTML tags and en special syntax so the boundary is clearer, but otherwise they don't tend to overlap since the symbols most used in HTML are not special in en. -If you want to avoid either one of these syntaxes from being interpreted specially, you should escape the relevant characters as explained in the next section. - -## Rendering unformatted text - -The backtick character `\\`` can be used to render unformatted blocks and inline text: - -` -The asterisk `*` is special in en markup syntax. -` - -Using the syntax above, the asterisk won't be interpreted as the start of bold formatting and instead will be shown like this: `*`. - -This is useful for code but also for rendering characters with special meaning you wish to mention literally. - -Backticks on their own line will start and close a block of unformatted text such as the ones being used throughout this documentation to show code: - -` -\\` -everything in here will be rendered without formatting -\\` -` - -Finally, you can precede any character with a `\\\\` to fully _escape_ that character from being interpreted. Because |TOML| also treats backslashes specially, you'll likely need to use double slashes, as in `\\\\\\\\`, unless you wrap your TOML strings in single quotes. See |Escaping| for more details and examples. +If you want to avoid either one of these syntaxes from being interpreted specially, you should escape the relevant characters as explained in the previous section. """ [nodes.Escaping] @@ -583,7 +668,7 @@ text = """ - [ ] Input syntax - [ ] Invert where redirects are set - [x] Formatting - - [ ] Blockquotes + - [x] Blockquotes - [ ] Tables - `%` block - newline for rows diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 328001a..0ea8c33 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -332,6 +332,17 @@ footer hr { margin-top: 60px; } +blockquote { + font-family: serifed; + font-size: calc(var(--base-font-size) * 1.6); +} + +.verse { + font-family: serifed; + font-size: calc(var(--base-font-size) * 1.6); + margin-left: 2em; +} + @media (prefers-color-scheme: dark) { * { background: #222222; From 260610c4a0db5c10490ce958850f89cdd7bdbe6d Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Feb 2026 17:08:16 -0300 Subject: [PATCH 014/108] Implement blockquote token --- .justfile | 2 +- Cargo.toml | 2 +- src/syntax/content/parser/context.rs | 13 ++- src/syntax/content/parser/context/block.rs | 12 +- src/syntax/content/parser/context/list.rs | 1 + src/syntax/content/parser/context/quote.rs | 97 ++++++++++++++++ src/syntax/content/parser/state.rs | 48 ++------ src/syntax/content/parser/token/quote.rs | 63 +++++----- static/graph.toml | 128 +++++++++++++++------ static/public/assets/style.css | 15 ++- templates/node.html | 2 +- 11 files changed, 263 insertions(+), 120 deletions(-) create mode 100644 src/syntax/content/parser/context/quote.rs diff --git a/.justfile b/.justfile index ad3be8c..16db5d1 100644 --- a/.justfile +++ b/.justfile @@ -18,7 +18,7 @@ alias r := run # Build and serve on changes [group: 'develop'] run-watch: - {{ watch_cmd }} {{ just_cmd }} run + @{{ watch_cmd }} {{ just_cmd }} run alias w := run-watch diff --git a/Cargo.toml b/Cargo.toml index a002498..f0e85d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ doc_broken_link = "warn" doc_comment_double_space_linebreaks = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" -empty_enum = "warn" +empty_enums = "warn" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index 7174ecf..094ffae 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -1,20 +1,21 @@ use crate::syntax::content::parser::{ State, Token, - token::{Header, Paragraph, PreFormat, Quote, Verse}, + token::{Header, Paragraph, PreFormat, Verse}, }; pub mod block; pub mod inline; pub mod anchor; pub mod list; +pub mod quote; -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct Context { pub block: Block, pub inline: Inline, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub enum Block { Paragraph, Header(u8), // level @@ -22,13 +23,15 @@ pub enum Block { PreFormat, Quote, Verse, + #[default] None, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub enum Inline { Anchor, Code, + #[default] None, } @@ -49,7 +52,7 @@ pub fn close(state: &State, tokens: &mut Vec) { tokens.push(Token::Header(Header::from_u8(level, false, None))); }, Block::Quote => { - tokens.push(Token::Quote(Quote::new(false))); + panic!("End of input with open quote") }, Block::Verse => { tokens.push(Token::Verse(Verse::new(false))); diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index 64b080d..16711bd 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -50,7 +50,7 @@ pub fn parse( } else if Quote::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Quote on {lexeme}"); state.context.block = Block::Quote; - tokens.push(Token::Quote(Quote::new(true))); + iterator.next(); return true; } else if Verse::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Verse on {lexeme}"); @@ -93,15 +93,7 @@ pub fn parse( return super::list::parse(lexeme, state, tokens, iterator, graph); }, Block::Quote => { - if lexeme.match_char_sequence('\n', '>') { - tokens.push(Token::LineBreak(LineBreak::default())); - iterator.next(); - return true; - } else if Quote::probe_end(lexeme) { - tokens.push(Token::Quote(Quote::new(false))); - log!(VERBOSE, "Block Context: Quote -> None on {lexeme}"); - state.context.block = Block::None; - } + return super::quote::parse(lexeme, state, tokens, iterator, graph); }, Block::Verse => { if Verse::probe_end(lexeme) { diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index bec908d..1fc938a 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -30,6 +30,7 @@ pub fn parse( let candidate = &mut buffer.candidate; let item_candidate = &mut buffer.item_candidate; + #[allow(clippy::wildcard_enum_match_arm)] match state.context.block { Block::List => { if lexeme.match_char(' ') && item_candidate.depth.is_none() { diff --git a/src/syntax/content/parser/context/quote.rs b/src/syntax/content/parser/context/quote.rs new file mode 100644 index 0000000..f09a8f7 --- /dev/null +++ b/src/syntax/content/parser/context/quote.rs @@ -0,0 +1,97 @@ +use std::{iter::Peekable, slice::Iter}; + +use crate::{ + graph::Graph, + prelude::*, + syntax::content::parser::{ + Lexeme, State, Token, + context::Block, + format, state, + token::{Anchor, Quote}, + }, +}; + +/// Handles open quote contexts until a quote is fully parsed. +/// +/// A return of `true` will trigger a continue in the outer parser, +/// skipping any further parsing of the current lexeme. +/// +/// # Panics +/// This parser can handle only the Quote context, and will panic if passed an +/// unrelated context since it has no knowledge on how to handle them. +pub fn parse( + lexeme: &Lexeme, + state: &mut State, + tokens: &mut Vec, + iterator: &mut Peekable>, + graph: &Graph, +) -> bool { + let buffer = &mut state.buffers.quote; + let candidate = &mut buffer.candidate; + + #[allow(clippy::wildcard_enum_match_arm)] + match state.context.block { + Block::Quote => { + if Quote::probe_end(lexeme) { + log!("Probed end of quote on {lexeme}"); + let (text, text_tokens) = format(&candidate.text, graph); + candidate.text = text; + state.format_tokens.extend_from_slice(&text_tokens); + + if let Some(citation) = &candidate.citation { + let (formatted_citation, citation_tokens) = + format(citation, graph); + candidate.citation = Some(formatted_citation); + state.format_tokens.extend_from_slice(&citation_tokens); + + let mut first_anchor = Anchor::default(); + for token in citation_tokens { + if let Token::Anchor(token_data) = token { + first_anchor = *token_data.clone(); + break; + } + } + if first_anchor.external() { + candidate.url = first_anchor.destination(); + } + } + + tokens.push(Token::Quote(candidate.clone())); + log!(VERBOSE, "Block Context: Quote -> None on {lexeme}"); + state.context.block = Block::None; + *buffer = state::QuoteBuffer::default(); + } else if !buffer.in_citation + && lexeme.match_char('\n') + && lexeme.next() == "--" + { + log!("Matched citation start on {lexeme}"); + buffer.in_citation = true; + iterator.next(); + iterator.next(); + } else if lexeme.match_char_sequence('\n', '>') { + log!("Matched break-aware sequence on {lexeme}"); + candidate.text.push_str(" <\n"); + iterator.next(); + } else { + log!("Entered quote else branch on {lexeme}"); + if buffer.in_citation { + log!("Extending citation on {lexeme}"); + candidate.extend_citation(&lexeme.text()); + if lexeme.match_char('\n') && lexeme.next() == "--" { + candidate.text.push('\n'); + iterator.next(); + } else if lexeme.match_char('\n') { + buffer.in_citation = false; + } + } else { + log!("Extending quote on {lexeme}"); + candidate.text.push_str(&lexeme.text()); + } + } + }, + _ => { + panic!("Quote context parser called to handle non-quote context") + }, + } + true +} diff --git a/src/syntax/content/parser/state.rs b/src/syntax/content/parser/state.rs index 851b0e7..0059548 100644 --- a/src/syntax/content/parser/state.rs +++ b/src/syntax/content/parser/state.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use crate::syntax::content::parser::{ Token, - context::{Block, Context, Inline}, - token::{Anchor, Item, List}, + context::Context, + token::{Anchor, Item, List, Quote}, }; -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct State { pub context: Context, pub dom_ids: HashMap>, @@ -15,7 +15,7 @@ pub struct State { pub format_tokens: Vec, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct Switches { pub bold: bool, pub oblique: bool, @@ -23,10 +23,11 @@ pub struct Switches { pub underline: bool, } -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub struct Buffers { pub anchor: AnchorBuffer, pub list: ListBuffer, + pub quote: QuoteBuffer, } #[derive(Default, Clone, Debug)] @@ -43,6 +44,12 @@ pub struct AnchorBuffer { pub destination: String, } +#[derive(Default, Clone, Debug)] +pub struct QuoteBuffer { + pub candidate: Quote, + pub in_citation: bool, +} + impl std::fmt::Display for AnchorBuffer { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let display_text = if self.text.is_empty() { @@ -67,37 +74,6 @@ impl std::fmt::Display for AnchorBuffer { } } -impl Default for State { - fn default() -> State { - State { - context: Context { - inline: Inline::None, - block: Block::None, - }, - dom_ids: HashMap::default(), - switches: Switches { - bold: false, - crossout: false, - oblique: false, - underline: false, - }, - buffers: Buffers { - anchor: AnchorBuffer { - candidate: Anchor::default(), - text: String::default(), - destination: String::default(), - }, - list: ListBuffer { - candidate: List::default(), - item_candidate: Item::default(), - depth: 0, - }, - }, - format_tokens: vec![], - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/syntax/content/parser/token/quote.rs b/src/syntax/content/parser/token/quote.rs index e9bdf49..325654e 100644 --- a/src/syntax/content/parser/token/quote.rs +++ b/src/syntax/content/parser/token/quote.rs @@ -1,18 +1,24 @@ use crate::syntax::content::{Parseable, parser::Lexeme}; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Eq, PartialEq)] pub struct Quote { - open: Option, + pub text: String, + pub citation: Option, + pub url: Option, } impl Quote { - pub fn new(open: bool) -> Quote { - Quote { open: Some(open) } - } - pub fn probe_end(lexeme: &Lexeme) -> bool { lexeme.match_char_sequence('\n', '\n') } + + pub fn extend_citation(&mut self, s: &str) { + if let Some(current) = &self.citation { + self.citation = Some(format!("{current}{s}")); + } else { + self.citation = Some(String::from(s)); + } + } } impl Parseable for Quote { @@ -21,19 +27,26 @@ impl Parseable for Quote { } fn lex(_lexeme: &Lexeme) -> Quote { - Quote { open: None } + Quote::default() } fn render(&self) -> String { - if let Some(open) = self.open { - if open { - "
".to_owned() - } else { - "
".to_owned() - } + let opening = if let Some(url) = &self.url { + format!(r#"
"#) } else { - panic!("Attempt to render a quote tag while open state is unknown") - } + String::from("
") + }; + + let content = if let Some(citation) = &self.citation { + format!( + r#"{}

{citation}

"#, + &self.text + ) + } else { + String::from(&self.text) + }; + + format!("\n{opening}\n{content}\n
\n") } fn flatten(&self) -> String { @@ -43,16 +56,14 @@ impl Parseable for Quote { impl std::fmt::Display for Quote { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let display_open_state = match self.open { - Some(open_state) => { - if open_state { - "open" - } else { - "closed" - } - }, - None => "unknown", - }; - write!(f, "Quote [{display_open_state}]") + let mut meta = String::default(); + if self.url.is_some() { + meta.push_str("+url "); + } + if self.citation.is_some() { + meta.push_str("+citation "); + } + + write!(f, "Quote [{}]", meta.trim()) } } diff --git a/static/graph.toml b/static/graph.toml index cb33cf5..7882407 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -188,7 +188,7 @@ If you are familiar with Markdown|https://en.wikipedia.org/wiki/Markdown|, you'l ## Anchors -Anchors have the following basic syntax: +Anchors are the most important and powerful syntactic element you will work with because they can create connections between nodes when you use them. They have the following basic syntax: ` anchor|destination @@ -197,11 +197,18 @@ anchor|destination For example: ` -DRC|DemocraticRepublicOfTheCongo +particles|ParticlePhysics +` + +This example will render as the word "particles" pointing to a node with ID `ParticlePhysics` because the destination is not an external URL. + +An external anchor looks like this: + +` docs|https://en.jutty.dev/node/Documentation ` -As shown above, anchors can point to external addresses. These are identified by the presence of either a `:` or a `/` character in the destination. Otherwise, the anchor will point to a node with an ID matching the destination. This means your anchors to external URLs, special handlers such as `mailto:user@domain.com` and destinations relative to the website root like `/about` will all work as intended without being interpreted as node IDs. +External anchors are identified by the presence of either a `:` or a `/` character in the destination. This works for special handlers, such as `mailto:user@domain.com`, and destinations relative to the website root like `/about`. If the left side contains spaces, you need a leading `|` character: @@ -223,6 +230,8 @@ For internal anchors, most punctuation is automatically separated from the ancho This gem|PreciousStone, though green, was not an emerald. ` +> This gem|PreciousStone, though green, was not an emerald. + However, for external anchors, you want to add a third `|` to explicitly set the end because external URLs can have all sorts of arbitrary characters. ### Node anchors @@ -235,14 +244,6 @@ A node ID wrapped in two `|` characters will become an anchor to that node: |ParticlePhysics| ` -And two words separated by a single anchor allow you to set a display text and destination: - -` -particles|ParticlePhysics -` - -This example will render as "particles|ParticlePhysics": the word particles pointing to a node with id `ParticlePhysics`. - en can resolve IDs case insensitively (with priority to case-sensitive matches) and will also collapse spaces when trying to resolve an ID, so you can also write: ` @@ -285,6 +286,9 @@ You can use `[ ]` and `[x]` to render checkboxes: - [x] done ` +- [ ] not done +- [x] done + ## Blocks A block is any group of lines separated by empty lines: @@ -292,12 +296,12 @@ A block is any group of lines separated by empty lines: ` block A still block A -still block A +block A's last line block B starts here +block B ends here -block C starts here -still block C +this is block C ` By default, a block not starting with any special syntax is a paragraph, such as this very line you are reading. @@ -310,7 +314,7 @@ b c ` -You still get "a b c" as a result. This is the case for paragraphs and blockquotes, but not for lists, verse blocks and preformatted text. +You still get "a b c" as a result. This is the case for paragraphs, but not for lists, verse blocks and preformatted text. Blockquotes support both modes. This is useful when editing your text, allowing you to break some thoughts and special syntax without losing control over where your paragraph ends, particularly when handling huge paragraphs. @@ -332,7 +336,7 @@ While useful to break a few lines on demand, if you have a large block of lines ### Verse -Verse blocks are delimited by a `&` character at their first and last line and are useful to avoid precisely this line-joining behavior of most blocks. They will break all lines without need for a trailing `<` character: +Verse blocks are delimited by a `&` character at their first and last line and are useful to avoid precisely this line-joining behavior of paragraphs. They will break all lines without need for a trailing `<` character: ` & @@ -342,8 +346,6 @@ Verse blocks are delimited by a `&` character at their first and last line and a & ` -This will be rendered as: - & these lines break just fine @@ -352,37 +354,79 @@ This will be rendered as: ### Quotes -A block of lines starting with a `>` character create a quote: +A block of lines starting with a `>` character will render as a quote: ` -> this is a quote +> Who'll change old lamps for new ones? ` +> Who'll change old lamps for new ones? + Quote blocks have two forms. If you prepend all blocks with a `>`, line breaks will be preserved, not collapsing the whole quote into a single line: -> a quote where all lines start with > BR -> still inside BR -> still inside BR -> -> above was a line with just a > plus a break BR -> still inside BR -> next is an empty line BR +` +> When I was alive +> I was dust which was, +> But now I am dust in dust +> I am dust which never was. +` + +> When I was alive +> I was dust which was, +> But now I am dust in dust +> I am dust which never was. If you would like the quote to be collapsed into a single line instead, you can leave just the first `>` and continue until the next empty line: -> this quote starts here -and continues here -until an empty line is found +` +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 +` + +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 You can still use `<` characters to force line breaks in this case. #### Citation -To add a citation to your qutoe block, you can add lines starting with two `-` characters: +To add a citation to your quote block, start a line with two `-` characters: -> a quote with _nested_ formatting *syntax and in* particular an anchor|Roadmap BR -> the quote continuation as it goes on and on --- quote by |Person Johnson|https://personjohnson.com +` +> with no more communion +> to down as morning pick-me-ups +> to sweeten afternoon naps +> to soothe nightmares +-- Assotto Saint, The Language of Dust +` + +> with no more communion +> to down as morning pick-me-ups +> to sweeten afternoon naps +> to soothe nightmares +-- Assotto Saint, The Language of Dust + +If you have a more complex citation, you can use multiple lines starting with `--`. All such lines will be joined together in the citation. If you need to break lines, use the `<` character at the end of a line: + +` +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, +-- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, +-- March 3rd, 1995, < +-- _Dia Internacional para a Eliminação da Discriminação Racial._ +` + +> Dois grandes mitos dominam a história oficial do Brasil: +o mito da índole pacífica do brasileiro e o da "democracia racial". +-- Benedita da Silva, +-- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, +-- March 3rd, 1995, < +-- Dia Internacional para a Eliminação da Discriminação Racial. + +The first URL found in your citation will be used as the blockquote element's `cite` field. ### Lists @@ -394,6 +438,10 @@ A block of lines starting with a `-` character will be rendered as an unordered - crimson ` +- cyan +- amber +- crimson + Lines starting with a `+` character will create numbered lists instead: ` @@ -402,6 +450,10 @@ Lines starting with a `+` character will create numbered lists instead: + san ` ++ ichi ++ ni ++ san + ## Rendering unformatted text The backtick character `\\`` can be used to render unformatted blocks and inline text: @@ -410,6 +462,8 @@ The backtick character `\\`` can be used to render unformatted blocks and inline The asterisk `*` is special in en markup syntax. ` +> The asterisk `*` is special in en markup syntax. + Using the syntax above, the asterisk won't be interpreted as the start of bold formatting and instead will be shown like this: `*`. This is useful for code but also for rendering characters with special meaning you wish to mention literally. @@ -437,8 +491,6 @@ If you need some more advanced feature that is not supported directly by en's ma </table> ` -Which will render to: - @@ -653,8 +705,8 @@ en is only possible thanks to a number of projects and people: - Neovim|https://neovim.io/ - foot|https://codeberg.org/dnkl/foot - tmux|https://github.com/tmux/tmux/ -- |Void Linux|https://voidlinux.org/ and its kernel|https://www.kernel.org/ -- LibreWolf|https://librewolf.net/ +- |Debian|https://debian.org/ and its kernel|https://www.kernel.org/ +- LibreWolf|https://librewolf.net/ and its upstream |Mozilla Firefox|https://www.firefox.com/ - InkScape|https://inkscape.org/ """ diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 0ea8c33..73d9f19 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -25,7 +25,7 @@ main { box-sizing: border-box; } -p, section li { +p:not(.quote-citation), section li { font-family: prose, sans-serif; } @@ -337,12 +337,23 @@ blockquote { font-size: calc(var(--base-font-size) * 1.6); } -.verse { +p.verse { font-family: serifed; font-size: calc(var(--base-font-size) * 1.6); margin-left: 2em; } +.quote-citation { + margin: 0.5em 0 0 0; + text-indent: -1.2em; + padding-left: 1em; + font-size: calc(var(--base-font-size) * 0.8); +} + +.quote-citation::before { + content: "— "; +} + @media (prefers-color-scheme: dark) { * { background: #222222; diff --git a/templates/node.html b/templates/node.html index 0c686e4..6d19a8a 100644 --- a/templates/node.html +++ b/templates/node.html @@ -44,7 +44,7 @@ {% endif %} {% if incoming %}
-

Incoming

+

Incoming

    {% for connection in incoming %}
  • From 55d2e37ce2007a1c87448d55b53c6c69feee4004 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Feb 2026 19:21:21 -0300 Subject: [PATCH 015/108] Simplify quote citation URL lookup --- src/syntax/content/parser/context/quote.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/syntax/content/parser/context/quote.rs b/src/syntax/content/parser/context/quote.rs index f09a8f7..4ecf652 100644 --- a/src/syntax/content/parser/context/quote.rs +++ b/src/syntax/content/parser/context/quote.rs @@ -4,10 +4,7 @@ use crate::{ graph::Graph, prelude::*, syntax::content::parser::{ - Lexeme, State, Token, - context::Block, - format, state, - token::{Anchor, Quote}, + Lexeme, State, Token, context::Block, format, state, token::Quote, }, }; @@ -44,15 +41,13 @@ pub fn parse( candidate.citation = Some(formatted_citation); state.format_tokens.extend_from_slice(&citation_tokens); - let mut first_anchor = Anchor::default(); - for token in citation_tokens { - if let Token::Anchor(token_data) = token { - first_anchor = *token_data.clone(); - break; - } - } - if first_anchor.external() { - candidate.url = first_anchor.destination(); + if let Some(url) = citation_tokens.into_iter().find_map( + |token| match token { + Token::Anchor(a) if a.external() => a.destination(), + _ => None, + }, + ) { + candidate.url = Some(url); } } From 0376d553562330c2d09a559d27eb6ac8558d6ae3 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Feb 2026 19:30:59 -0300 Subject: [PATCH 016/108] Fix mismatched example in Syntax page --- static/graph.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/graph.toml b/static/graph.toml index 7882407..2b7e303 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -416,7 +416,7 @@ o mito da índole pacífica do brasileiro e o da "democracia racial". -- Benedita da Silva, -- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, -- March 3rd, 1995, < --- _Dia Internacional para a Eliminação da Discriminação Racial._ +-- International Day for the Elimination of Racial Discrimination. ` > Dois grandes mitos dominam a história oficial do Brasil: From ed4d703d4681b5569468182c251277368105e7a4 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Feb 2026 19:37:51 -0300 Subject: [PATCH 017/108] Adjust spacing on quote syntax examples --- static/graph.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/graph.toml b/static/graph.toml index 2b7e303..26c50ea 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -380,12 +380,12 @@ If you would like the quote to be collapsed into a single line instead, you can ` > Dois grandes mitos dominam a história oficial do Brasil: -o mito da índole pacífica do brasileiro e o da "democracia racial". + o mito da índole pacífica do brasileiro e o da "democracia racial". -- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 ` > Dois grandes mitos dominam a história oficial do Brasil: -o mito da índole pacífica do brasileiro e o da "democracia racial". + o mito da índole pacífica do brasileiro e o da "democracia racial". -- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 You can still use `<` characters to force line breaks in this case. @@ -412,7 +412,7 @@ If you have a more complex citation, you can use multiple lines starting with `- ` > Dois grandes mitos dominam a história oficial do Brasil: -o mito da índole pacífica do brasileiro e o da "democracia racial". + o mito da índole pacífica do brasileiro e o da "democracia racial". -- Benedita da Silva, -- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, -- March 3rd, 1995, < @@ -420,7 +420,7 @@ o mito da índole pacífica do brasileiro e o da "democracia racial". ` > Dois grandes mitos dominam a história oficial do Brasil: -o mito da índole pacífica do brasileiro e o da "democracia racial". + o mito da índole pacífica do brasileiro e o da "democracia racial". -- Benedita da Silva, -- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, -- March 3rd, 1995, < From 39c7373e7e69d22c6b67f1e788f7a6e998f46bb6 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 9 Feb 2026 01:47:23 -0300 Subject: [PATCH 018/108] Simplify a quote example to not have a citation --- static/graph.toml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/static/graph.toml b/static/graph.toml index 26c50ea..fe3a3c7 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -379,14 +379,16 @@ Quote blocks have two forms. If you prepend all blocks with a `>`, line breaks w If you would like the quote to be collapsed into a single line instead, you can leave just the first `>` and continue until the next empty line: ` -> Dois grandes mitos dominam a história oficial do Brasil: - o mito da índole pacífica do brasileiro e o da "democracia racial". --- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 +> And should I feel kindness towards my enemies? +No: from that moment I declared everlasting war against the species, +and more than all, against him who had formed me +and sent me forth to this insupportable misery. ` -> Dois grandes mitos dominam a história oficial do Brasil: - o mito da índole pacífica do brasileiro e o da "democracia racial". --- Benedita da Silva, Speech on the Federal Senate, March 3rd, 1995 +> And should I feel kindness towards my enemies? +No: from that moment I declared everlasting war against the species, +and more than all, against him who had formed me +and sent me forth to this insupportable misery. You can still use `<` characters to force line breaks in this case. @@ -426,7 +428,7 @@ If you have a more complex citation, you can use multiple lines starting with `- -- March 3rd, 1995, < -- Dia Internacional para a Eliminação da Discriminação Racial. -The first URL found in your citation will be used as the blockquote element's `cite` field. +The first URL found in your citation will be used as the blockquote element's `cite` value. ### Lists From 834949939a398bbe0896146d36f7e2d9ac60b28e Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 9 Feb 2026 20:39:21 -0300 Subject: [PATCH 019/108] Extract lexer to its own module --- src/syntax/content/parser.rs | 82 ++------------------------- src/syntax/content/parser/lexer.rs | 90 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 77 deletions(-) create mode 100644 src/syntax/content/parser/lexer.rs diff --git a/src/syntax/content/parser.rs b/src/syntax/content/parser.rs index 0498582..0a3c0f0 100644 --- a/src/syntax/content/parser.rs +++ b/src/syntax/content/parser.rs @@ -1,86 +1,18 @@ -use crate::{prelude::*, graph::Graph}; -use super::{TokenOutput, Parseable as _, LexMap}; -use token::{LineBreak, Literal}; +use crate::{prelude::*, graph::Graph, syntax::content::TokenOutput}; use context::{Block, Inline}; +use lexer::{LEXMAP, lex}; pub use {lexeme::Lexeme, token::Token, state::State}; pub mod token; +pub mod lexer; pub mod lexeme; pub mod segment; pub mod context; pub mod point; pub mod state; -const LEXMAP: LexMap = &[ - (LineBreak::probe, |lexeme| { - Token::LineBreak(LineBreak::lex(lexeme)) - }), - (Literal::probe, |lexeme| { - Token::Literal(Literal::lex(lexeme)) - }), -]; - -fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput { - let mut tokens: Vec = Vec::default(); - let mut state = State::default(); - - let segments = segment::segment(text); - let lexemes = Lexeme::collect(&segments); - - log!(VERBOSE, "Segments: {segments:?}"); - - let mut iterator = lexemes.iter().peekable(); - while let Some(lexeme) = iterator.next() { - if lexeme.match_char('\\') { - if let Some(next) = iterator.next() { - tokens.push(Token::Literal(Literal::lex(next))); - } - continue; - } - - if blocking { - if context::block::parse( - lexeme, - &mut state, - &mut tokens, - &mut iterator, - graph, - ) { - continue; - } - } - - if point::parse(lexeme, &mut state, &mut tokens, &mut iterator) { - continue; - } - - if context::inline::parse( - lexeme, - &mut state, - &mut tokens, - &mut iterator, - graph, - ) { - continue; - } - - for (probe, lex) in map { - if probe(lexeme) { - let token = lex(lexeme); - log!(VERBOSE, "Lexmap lexed {lexeme} into {token}"); - tokens.push(token); - break; - } - } - } - - context::close(&state, &mut tokens); - - TokenOutput { - tokens, - format_tokens: state.format_tokens, - text: None, - } +fn parse(tokens: &[Token]) -> String { + tokens.iter().map(Token::render).collect::() } pub(super) fn read(input: &str, graph: &Graph) -> String { @@ -112,10 +44,6 @@ pub fn flatten(input: &str, graph: &Graph) -> String { flat } -fn parse(tokens: &[Token]) -> String { - tokens.iter().map(Token::render).collect::() -} - #[cfg(test)] mod tests { use crate::{ diff --git a/src/syntax/content/parser/lexer.rs b/src/syntax/content/parser/lexer.rs new file mode 100644 index 0000000..b87d868 --- /dev/null +++ b/src/syntax/content/parser/lexer.rs @@ -0,0 +1,90 @@ +use crate::{ + prelude::*, + graph::Graph, + syntax::content::{ + TokenOutput, Parseable as _, LexMap, + parser::{ + lexeme::Lexeme, + token::{Token, LineBreak, Literal}, + state::State, + segment, context, point, + }, + }, +}; + +pub(super) const LEXMAP: LexMap = &[ + (LineBreak::probe, |lexeme| { + Token::LineBreak(LineBreak::lex(lexeme)) + }), + (Literal::probe, |lexeme| { + Token::Literal(Literal::lex(lexeme)) + }), +]; + +pub(super) fn lex( + text: &str, + map: LexMap, + graph: &Graph, + blocking: bool, +) -> TokenOutput { + let mut tokens: Vec = Vec::default(); + let mut state = State::default(); + + let segments = segment::segment(text); + let lexemes = Lexeme::collect(&segments); + + log!(VERBOSE, "Segments: {segments:?}"); + + let mut iterator = lexemes.iter().peekable(); + while let Some(lexeme) = iterator.next() { + if lexeme.match_char('\\') { + if let Some(next) = iterator.next() { + tokens.push(Token::Literal(Literal::lex(next))); + } + continue; + } + + if blocking { + if context::block::parse( + lexeme, + &mut state, + &mut tokens, + &mut iterator, + graph, + ) { + continue; + } + } + + if point::parse(lexeme, &mut state, &mut tokens, &mut iterator) { + continue; + } + + if context::inline::parse( + lexeme, + &mut state, + &mut tokens, + &mut iterator, + graph, + ) { + continue; + } + + for (probe, lex) in map { + if probe(lexeme) { + let token = lex(lexeme); + log!(VERBOSE, "Lexmap lexed {lexeme} into {token}"); + tokens.push(token); + break; + } + } + } + + context::close(&state, &mut tokens); + + TokenOutput { + tokens, + format_tokens: state.format_tokens, + text: None, + } +} From 898708691a626e11c5d790164c480d52a2c4c143 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 9 Feb 2026 20:59:46 -0300 Subject: [PATCH 020/108] Revert to text decoration for visited link distinctions --- static/public/assets/style.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 73d9f19..50d7da6 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -69,8 +69,8 @@ pre, code { a { color: #0f6366; - text-decoration: underline dotted #138e8e; - text-decoration-thickness: 1.5px; + text-decoration: underline dotted #159b9b; + text-decoration-thickness: 2.5px; text-underline-offset: 3px; transition: 1500ms; } @@ -92,6 +92,7 @@ a.external { color: #1958a7; text-decoration-color: #2A7CDF; text-decoration-style: solid; + text-decoration-thickness: 1.5px; transition: 1500ms; } @@ -105,8 +106,7 @@ a:visited, a.detached:visited, a.external:visited { - text-decoration-color: #999; - color: #7e20cf; + text-decoration-color: #bbb; transition: 1500ms; } @@ -367,7 +367,6 @@ p.verse { a { color: #1dd7d7; - text-decoration-color: #159b9b; transition: 1500ms; } @@ -413,7 +412,6 @@ p.verse { a.external:visited { text-decoration-color: #999; - color: #a3a5ff; transition: 1500ms; } From 6f8567e5bab6e8228ddbc04073e04ab5e6c3bc01 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 9 Feb 2026 21:02:15 -0300 Subject: [PATCH 021/108] Make quote logging verbose --- src/syntax/content/parser/context/quote.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/syntax/content/parser/context/quote.rs b/src/syntax/content/parser/context/quote.rs index 4ecf652..08664c1 100644 --- a/src/syntax/content/parser/context/quote.rs +++ b/src/syntax/content/parser/context/quote.rs @@ -30,7 +30,7 @@ pub fn parse( match state.context.block { Block::Quote => { if Quote::probe_end(lexeme) { - log!("Probed end of quote on {lexeme}"); + log!(VERBOSE, "Probed end of quote on {lexeme}"); let (text, text_tokens) = format(&candidate.text, graph); candidate.text = text; state.format_tokens.extend_from_slice(&text_tokens); @@ -59,18 +59,18 @@ pub fn parse( && lexeme.match_char('\n') && lexeme.next() == "--" { - log!("Matched citation start on {lexeme}"); + log!(VERBOSE, "Matched citation start on {lexeme}"); buffer.in_citation = true; iterator.next(); iterator.next(); } else if lexeme.match_char_sequence('\n', '>') { - log!("Matched break-aware sequence on {lexeme}"); + log!(VERBOSE, "Matched break-aware sequence on {lexeme}"); candidate.text.push_str(" <\n"); iterator.next(); } else { - log!("Entered quote else branch on {lexeme}"); + log!(VERBOSE, "Entered quote else branch on {lexeme}"); if buffer.in_citation { - log!("Extending citation on {lexeme}"); + log!(VERBOSE, "Extending citation on {lexeme}"); candidate.extend_citation(&lexeme.text()); if lexeme.match_char('\n') && lexeme.next() == "--" { candidate.text.push('\n'); @@ -79,7 +79,7 @@ pub fn parse( buffer.in_citation = false; } } else { - log!("Extending quote on {lexeme}"); + log!(VERBOSE, "Extending quote on {lexeme}"); candidate.text.push_str(&lexeme.text()); } } From 4187299e04055db9f3063cbf6ff3abf86c212ef3 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 9 Feb 2026 21:42:51 -0300 Subject: [PATCH 022/108] Use a cite element for blockquote citations --- src/syntax/content/parser/token/quote.rs | 2 +- static/public/assets/style.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/syntax/content/parser/token/quote.rs b/src/syntax/content/parser/token/quote.rs index 325654e..ab0bd36 100644 --- a/src/syntax/content/parser/token/quote.rs +++ b/src/syntax/content/parser/token/quote.rs @@ -39,7 +39,7 @@ impl Parseable for Quote { let content = if let Some(citation) = &self.citation { format!( - r#"{}

    {citation}

    "#, + r#"{}
    {citation}"#, &self.text ) } else { diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 50d7da6..509b110 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -344,6 +344,7 @@ p.verse { } .quote-citation { + display: block; margin: 0.5em 0 0 0; text-indent: -1.2em; padding-left: 1em; From b890eb93f14b32b0441c70489069fe21be23faff Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 01:06:24 -0300 Subject: [PATCH 023/108] Scaffold table token --- src/syntax/content/parser/context.rs | 5 + src/syntax/content/parser/context/block.rs | 10 +- src/syntax/content/parser/context/table.rs | 114 +++++++++++++++++++++ src/syntax/content/parser/state.rs | 11 +- src/syntax/content/parser/token.rs | 9 +- src/syntax/content/parser/token/table.rs | 72 +++++++++++++ 6 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 src/syntax/content/parser/context/table.rs create mode 100644 src/syntax/content/parser/token/table.rs diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index 094ffae..861b9c1 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -8,6 +8,7 @@ pub mod inline; pub mod anchor; pub mod list; pub mod quote; +pub mod table; #[derive(Clone, Default, Debug)] pub struct Context { @@ -22,6 +23,7 @@ pub enum Block { List, PreFormat, Quote, + Table, Verse, #[default] None, @@ -54,6 +56,9 @@ pub fn close(state: &State, tokens: &mut Vec) { Block::Quote => { panic!("End of input with open quote") }, + Block::Table => { + panic!("End of input with open table") + }, Block::Verse => { tokens.push(Token::Verse(Verse::new(false))); }, diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index 16711bd..292bdf0 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -9,7 +9,7 @@ use crate::{ Block, Lexeme, State, Token, token::{ Header, List, LineBreak, Literal, Paragraph, PreFormat, Quote, - Verse, + Table, Verse, }, }, }, @@ -59,6 +59,11 @@ pub fn parse( iterator.next(); iterator.next(); return true; + } else if Table::probe(lexeme) { + log!(VERBOSE, "Block Context: None -> Table on {lexeme}"); + state.context.block = Block::Table; + iterator.next(); + return true; } else if Paragraph::probe(lexeme) { log!(VERBOSE, "Block Context: None -> Paragraph on {lexeme}"); state.context.block = Block::Paragraph; @@ -95,6 +100,9 @@ pub fn parse( Block::Quote => { return super::quote::parse(lexeme, state, tokens, iterator, graph); }, + Block::Table => { + return super::table::parse(lexeme, state, tokens, iterator, graph); + }, Block::Verse => { if Verse::probe_end(lexeme) { tokens.push(Token::Verse(Verse::new(false))); diff --git a/src/syntax/content/parser/context/table.rs b/src/syntax/content/parser/context/table.rs new file mode 100644 index 0000000..0aa95d2 --- /dev/null +++ b/src/syntax/content/parser/context/table.rs @@ -0,0 +1,114 @@ +use std::{iter::Peekable, slice::Iter}; + +use crate::{ + graph::Graph, + prelude::*, + syntax::content::parser::{ + Lexeme, State, Token, context::Block, format, state, token::Table, + }, +}; + +/// Handles open table contexts until a table is fully parsed. +/// +/// A return of `true` will trigger a continue in the outer parser, +/// skipping any further parsing of the current lexeme. +/// +/// # Panics +/// This parser can handle only the Table context, and will panic if passed an +/// unrelated context since it has no knowledge on how to handle them. +pub fn parse( + lexeme: &Lexeme, + state: &mut State, + tokens: &mut Vec, + iterator: &mut Peekable>, + graph: &Graph, +) -> bool { + let buffer = &mut state.buffers.table; + let candidate = &mut buffer.candidate; + + let mut parse_text = |text: &str| { + let (parsed_text, text_tokens) = format(text, graph); + state.format_tokens.extend_from_slice(&text_tokens); + parsed_text + }; + + #[allow(clippy::wildcard_enum_match_arm)] + match state.context.block { + Block::Table => { + if Table::probe_end(lexeme) { + log!(VERBOSE, "Probed end of table on {lexeme}"); + + if buffer.in_header { + log!(VERBOSE, "Adding unterminated header: {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else if buffer.in_cell { + log!(VERBOSE, "Adding unterminated cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else { + log!(VERBOSE, "Adding undelimited cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } + + tokens.push(Token::Table(candidate.clone())); + log!(VERBOSE, "Block Context: Table -> None on {lexeme}"); + state.context.block = Block::None; + *buffer = state::TableBuffer::default(); + iterator.next(); + } else if lexeme.match_char('\n') { + log!(VERBOSE, "Adding row: found newline on {lexeme}"); + if !buffer.cell.is_empty() { + + if buffer.in_header { + log!(VERBOSE, "Adding unterminated header: {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else if buffer.in_cell { + log!(VERBOSE, "Adding unterminated cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else { + log!(VERBOSE, "Adding undelimited cell: {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } + + } + candidate.add_row(vec![]); + } else if lexeme.match_char_triple(' ', '!', ' ') { + log!(VERBOSE, "Adding header: found spaced ! on {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else if lexeme.match_char_triple(' ', '|', ' ') { + log!(VERBOSE, "Adding cell: found spaced | on {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + iterator.next(); + iterator.next(); + } else { + log!(VERBOSE, "Extending cell text on {lexeme}"); + buffer.cell.push_str(lexeme.text().as_str()); + } + }, + _ => { + panic!("Table context parser called to handle non-table context") + }, + } + true +} diff --git a/src/syntax/content/parser/state.rs b/src/syntax/content/parser/state.rs index 0059548..cea6f6f 100644 --- a/src/syntax/content/parser/state.rs +++ b/src/syntax/content/parser/state.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::syntax::content::parser::{ Token, context::Context, - token::{Anchor, Item, List, Quote}, + token::{Anchor, Item, List, Quote, Table}, }; #[derive(Clone, Default, Debug)] @@ -28,6 +28,7 @@ pub struct Buffers { pub anchor: AnchorBuffer, pub list: ListBuffer, pub quote: QuoteBuffer, + pub table: TableBuffer, } #[derive(Default, Clone, Debug)] @@ -50,6 +51,14 @@ pub struct QuoteBuffer { pub in_citation: bool, } +#[derive(Default, Clone, Debug)] +pub struct TableBuffer { + pub candidate: Table, + pub cell: String, + pub in_cell: bool, + pub in_header: bool, +} + impl std::fmt::Display for AnchorBuffer { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let display_text = if self.text.is_empty() { diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index a64f48b..cfbb39b 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -14,14 +14,15 @@ pub mod paragraph; pub mod preformat; pub mod quote; pub mod strike; +pub mod table; pub mod underline; pub mod verse; pub use { anchor::Anchor, bold::Bold, checkbox::CheckBox, code::Code, header::Header, item::Item, linebreak::LineBreak, list::List, literal::Literal, - oblique::Oblique, paragraph::Paragraph, preformat::PreFormat, - strike::Strike, underline::Underline, quote::Quote, verse::Verse, + oblique::Oblique, paragraph::Paragraph, preformat::PreFormat, quote::Quote, + strike::Strike, table::Table, underline::Underline, verse::Verse, }; #[derive(Debug, Eq, PartialEq, Clone)] @@ -40,6 +41,7 @@ pub enum Token { Paragraph(Paragraph), PreFormat(PreFormat), Quote(Quote), + Table(Table), Underline(Underline), Verse(Verse), } @@ -61,6 +63,7 @@ impl Token { Token::Paragraph(d) => d.render(), Token::PreFormat(d) => d.render(), Token::Quote(d) => d.render(), + Token::Table(d) => d.render(), Token::Underline(d) => d.render(), Token::Verse(d) => d.render(), } @@ -82,6 +85,7 @@ impl Token { Token::Paragraph(d) => d.flatten(), Token::PreFormat(d) => d.flatten(), Token::Quote(d) => d.flatten(), + Token::Table(d) => d.flatten(), Token::Underline(d) => d.flatten(), Token::Verse(d) => d.flatten(), } @@ -105,6 +109,7 @@ impl std::fmt::Display for Token { Token::Paragraph(d) => format!("{d}"), Token::PreFormat(d) => format!("{d}"), Token::Quote(d) => format!("{d}"), + Token::Table(d) => format!("{d}"), Token::Underline(d) => format!("{d}"), Token::Verse(d) => format!("{d}"), }; diff --git a/src/syntax/content/parser/token/table.rs b/src/syntax/content/parser/token/table.rs new file mode 100644 index 0000000..8d0b811 --- /dev/null +++ b/src/syntax/content/parser/token/table.rs @@ -0,0 +1,72 @@ +use crate::syntax::content::{Parseable, parser::Lexeme}; + +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct Table { + pub headers: Vec, + pub contents: Vec>, +} + +impl Table { + pub fn probe_end(lexeme: &Lexeme) -> bool { + lexeme.match_char_triple('\n', '%', '\n') || lexeme.last() + } + + pub fn add_header(&mut self, header: &str) { + self.headers.push(header.trim().to_string()); + } + + pub fn add_row(&mut self, row: Vec) { + self.contents.push(row); + } + + pub fn add_cell(&mut self, content: &str) { + if let Some(last) = self.contents.last_mut() { + last.push(content.trim().to_string()); + } + } +} + +impl Parseable for Table { + fn probe(lexeme: &Lexeme) -> bool { + lexeme.match_char_triple('\n', '%', '\n') + } + + fn lex(_lexeme: &Lexeme) -> Table { + Table::default() + } + + fn render(&self) -> String { + let mut xml = String::from("\n
Hi,
\n"); + + if !self.headers.is_empty() { + xml.push_str("\n"); + for header in &self.headers { + xml.push_str(format!("\n").as_str()); + } + xml.push_str("\n\n"); + } + + for row in &self.contents { + if !row.is_empty() { + xml.push_str("\n"); + for cell in row { + xml.push_str(format!("\n").as_str()); + } + xml.push_str("\n\n"); + } + } + + xml.push_str("\n
{header}
{cell}
\n"); + xml + } + + fn flatten(&self) -> String { + String::default() + } +} + +impl std::fmt::Display for Table { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Table") + } +} From 6b7123b1ad43e5b012546aafb9e173e9a4ae0efc Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 01:54:19 -0300 Subject: [PATCH 024/108] Handle table cell separator cases --- src/syntax/content/parser/context/table.rs | 72 ++++++++++++---------- src/syntax/content/parser/token/table.rs | 29 ++++++--- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/syntax/content/parser/context/table.rs b/src/syntax/content/parser/context/table.rs index 0aa95d2..a4c3f8e 100644 --- a/src/syntax/content/parser/context/table.rs +++ b/src/syntax/content/parser/context/table.rs @@ -41,21 +41,14 @@ pub fn parse( if buffer.in_header { log!(VERBOSE, "Adding unterminated header: {lexeme}"); candidate.add_header(&parse_text(&buffer.cell)); - buffer.cell.clear(); - iterator.next(); - iterator.next(); - } else if buffer.in_cell { - log!(VERBOSE, "Adding unterminated cell: {lexeme}"); - candidate.add_cell(&parse_text(&buffer.cell)); - buffer.cell.clear(); - iterator.next(); - iterator.next(); } else { - log!(VERBOSE, "Adding undelimited cell: {lexeme}"); + let descriptor = if buffer.in_cell { + "unterminated" + } else { + "undelimited" + }; + log!(VERBOSE, "Adding {descriptor} cell: {lexeme}"); candidate.add_cell(&parse_text(&buffer.cell)); - buffer.cell.clear(); - iterator.next(); - iterator.next(); } tokens.push(Token::Table(candidate.clone())); @@ -63,42 +56,53 @@ pub fn parse( state.context.block = Block::None; *buffer = state::TableBuffer::default(); iterator.next(); - } else if lexeme.match_char('\n') { + } else if lexeme.match_char('\n') + || lexeme.match_char_triple(' ', '!', '\n') + || lexeme.match_char_triple(' ', '|', '\n') + { log!(VERBOSE, "Adding row: found newline on {lexeme}"); + if !buffer.cell.is_empty() { if buffer.in_header { log!(VERBOSE, "Adding unterminated header: {lexeme}"); candidate.add_header(&parse_text(&buffer.cell)); - buffer.cell.clear(); - iterator.next(); - iterator.next(); - } else if buffer.in_cell { - log!(VERBOSE, "Adding unterminated cell: {lexeme}"); - candidate.add_cell(&parse_text(&buffer.cell)); - buffer.cell.clear(); - iterator.next(); - iterator.next(); } else { - log!(VERBOSE, "Adding undelimited cell: {lexeme}"); + let descriptor = if buffer.in_cell { + "unterminated" + } else { + "undelimited" + }; + log!(VERBOSE, "Adding {descriptor} cell: {lexeme}"); candidate.add_cell(&parse_text(&buffer.cell)); - buffer.cell.clear(); - iterator.next(); - iterator.next(); } - + buffer.cell.clear(); } + + if lexeme.match_next_either_char('|', '!') { + iterator.next(); + } + + buffer.in_header = false; + buffer.in_cell = false; candidate.add_row(vec![]); + } else if lexeme.match_char_triple(' ', '!', ' ') { - log!(VERBOSE, "Adding header: found spaced ! on {lexeme}"); - candidate.add_header(&parse_text(&buffer.cell)); - buffer.cell.clear(); + buffer.in_header = true; + if !buffer.cell.trim().is_empty() { + log!(VERBOSE, "Adding header: found spaced ! on {lexeme}"); + candidate.add_header(&parse_text(&buffer.cell)); + buffer.cell.clear(); + } iterator.next(); iterator.next(); } else if lexeme.match_char_triple(' ', '|', ' ') { - log!(VERBOSE, "Adding cell: found spaced | on {lexeme}"); - candidate.add_cell(&parse_text(&buffer.cell)); - buffer.cell.clear(); + buffer.in_cell = true; + if !buffer.cell.trim().is_empty() { + log!(VERBOSE, "Adding cell: found spaced | on {lexeme}"); + candidate.add_cell(&parse_text(&buffer.cell)); + buffer.cell.clear(); + } iterator.next(); iterator.next(); } else { diff --git a/src/syntax/content/parser/token/table.rs b/src/syntax/content/parser/token/table.rs index 8d0b811..6b13f7a 100644 --- a/src/syntax/content/parser/token/table.rs +++ b/src/syntax/content/parser/token/table.rs @@ -24,11 +24,19 @@ impl Table { last.push(content.trim().to_string()); } } + + pub fn last_row_count(&self) -> usize { + if let Some(last) = self.contents.last() { + last.len() + } else { + 0 + } + } } impl Parseable for Table { fn probe(lexeme: &Lexeme) -> bool { - lexeme.match_char_triple('\n', '%', '\n') + lexeme.match_char_sequence('%', '\n') } fn lex(_lexeme: &Lexeme) -> Table { @@ -37,26 +45,29 @@ impl Parseable for Table { fn render(&self) -> String { let mut xml = String::from("\n\n"); + let tab = " "; if !self.headers.is_empty() { - xml.push_str("\n"); + xml.push_str(format!("{tab}\n").as_str()); for header in &self.headers { - xml.push_str(format!("\n").as_str()); + xml.push_str(format!("{tab}{tab}\n").as_str()); } - xml.push_str("\n\n"); + xml.push_str(format!("{tab}\n").as_str()); } for row in &self.contents { - if !row.is_empty() { - xml.push_str("\n"); + if !row.is_empty() && row.iter().any(|cell| !cell.is_empty()) { + xml.push_str(format!("{tab}\n").as_str()); for cell in row { - xml.push_str(format!("\n").as_str()); + xml.push_str( + format!("{tab}{tab}\n").as_str(), + ); } - xml.push_str("\n\n"); + xml.push_str(format!("{tab}\n").as_str()); } } - xml.push_str("\n
{header}{header}
{cell}{cell}
\n"); + xml.push_str("\n"); xml } From c116f6c1cb340b2224968b1bd0a02b1097d75564 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 12:54:47 -0300 Subject: [PATCH 025/108] Handle headerless tables --- src/syntax/content/parser/token/table.rs | 2 ++ static/public/assets/style.css | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/syntax/content/parser/token/table.rs b/src/syntax/content/parser/token/table.rs index 6b13f7a..cf5cb51 100644 --- a/src/syntax/content/parser/token/table.rs +++ b/src/syntax/content/parser/token/table.rs @@ -22,6 +22,8 @@ impl Table { pub fn add_cell(&mut self, content: &str) { if let Some(last) = self.contents.last_mut() { last.push(content.trim().to_string()); + } else { + self.contents.push(vec![content.trim().to_string()]); } } diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 509b110..519fb8d 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -309,11 +309,17 @@ em#index-node-count { } table { - margin: auto; + margin: 20px auto; border-collapse: collapse; border: 0.5px dotted #666; } +table th { + background: #099; + color: #fff; + border-color: #222; +} + td, th { padding: 10px; border: 0.5px dotted #666; @@ -437,6 +443,11 @@ p.verse { border-width: 1px; } + table th { + background: #002929; + border-color: #666; + } + } @media (max-width: 600px) { From 31db91a631b048ad95f7774d6cc37f2b077bea4a Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 13:36:48 -0300 Subject: [PATCH 026/108] Add table context handling tests --- src/syntax/content/parser/context/table.rs | 330 ++++++++++++++++++++- 1 file changed, 328 insertions(+), 2 deletions(-) diff --git a/src/syntax/content/parser/context/table.rs b/src/syntax/content/parser/context/table.rs index a4c3f8e..357f599 100644 --- a/src/syntax/content/parser/context/table.rs +++ b/src/syntax/content/parser/context/table.rs @@ -63,7 +63,6 @@ pub fn parse( log!(VERBOSE, "Adding row: found newline on {lexeme}"); if !buffer.cell.is_empty() { - if buffer.in_header { log!(VERBOSE, "Adding unterminated header: {lexeme}"); candidate.add_header(&parse_text(&buffer.cell)); @@ -86,7 +85,6 @@ pub fn parse( buffer.in_header = false; buffer.in_cell = false; candidate.add_row(vec![]); - } else if lexeme.match_char_triple(' ', '!', ' ') { buffer.in_header = true; if !buffer.cell.trim().is_empty() { @@ -116,3 +114,331 @@ pub fn parse( } true } + +#[cfg(test)] +mod tests { + use crate::{syntax::content::parser, graph::Graph}; + + fn read(input: &str) -> String { + parser::read(input, &Graph::default()) + } + + fn read_loaded(input: &str) -> String { + parser::read(input, &Graph::load()) + } + + #[test] + fn single_row() { + assert_eq!( + read(concat!("%", "\n", "a | b | c", "\n", "%", "\n")), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
", "\n", + ) + ); + } + + #[test] + fn two_rows() { + assert_eq!( + read(concat!( + "%", "\n", + "a | b | c", "\n", + "d | e | f", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
", "\n", + ) + ); + } + + #[test] + fn three_rows() { + assert_eq!( + read(concat!( + "%", "\n", + "a | b | c", "\n", + "d | e | f", "\n", + "g | h | i", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
ghi
", "\n", + ) + ); + } + + #[test] + fn with_header() { + assert_eq!( + read(concat!( + "%", "\n", + "hA ! hB ! hC", "\n", + "a | b | c", "\n", + "d | e | f", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
hAhBhC
abc
def
", "\n", + ) + ); + } + + #[test] + fn with_anchor() { + assert_eq!( + read(concat!( + "%", "\n", + "a | |Node| | c", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + r#" "#, "\n", + " ", "\n", + " ", "\n", + "
aNodec
", "\n", + )); + } + + #[test] + fn with_loaded_anchor() { + assert_eq!( + read_loaded(concat!( + "%", "\n", + "a | |Node| | c", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + r#" "#, "\n", + " ", "\n", + " ", "\n", + "
aNodec
", "\n", + )); + } + + #[test] + fn no_leading_delimiters() { + assert_eq!( + read_loaded(concat!( + "%", "\n", + "a ! b ! c !", "\n", + "d | e | f |", "\n", + "g | h | i |", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
ghi
", "\n", + ) + ); + } + + #[test] + fn no_trailing_delimiters() { + assert_eq!( + read_loaded(concat!( + "%", "\n", + " ! a ! b ! c", "\n", + " | d | e | f", "\n", + " | g | h | i", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
ghi
", "\n", + ) + ); + } + + #[test] + fn with_leading_and_trailing_delimiters() { + assert_eq!( + read_loaded(concat!( + "%", "\n", + " ! a ! b ! c !", "\n", + " | d | e | f |", "\n", + " | g | h | i |", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
ghi
", "\n", + ) + ); + } + + #[test] + fn no_flanking_delimiters() { + assert_eq!( + read_loaded(concat!( + "%", "\n", + "a ! b ! c", "\n", + "d | e | f", "\n", + "g | h | i", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
ghi
", "\n", + ) + ); + } + + #[test] + fn with_indent() { + assert_eq!( + read_loaded(concat!( + "%", "\n", + " ! a ! b ! c !", "\n", + " | d | e | f |", "\n", + " | g | h | i |", "\n", + "%", "\n", + )), + concat!( + "\n", + "", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + " ", "\n", + "
abc
def
ghi
", "\n", + ) + ); + } +} From d5e79daeeed613a049d27d0e9d97f1a707fba89d Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 13:58:24 -0300 Subject: [PATCH 027/108] Adopt nightly rustfmt --- .justfile | 4 ++-- .rustfmt.toml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.justfile b/.justfile index 16db5d1..0b23ad4 100644 --- a/.justfile +++ b/.justfile @@ -62,7 +62,7 @@ alias qo := quick-test-cover-watch # Format all files [group: 'develop'] format: - cargo fmt + cargo +nightly fmt alias f := format @@ -162,7 +162,7 @@ alias do := doc-open # Assess formatting [group: 'assess'] format-assess: - cargo fmt -- --check + cargo +nightly fmt -- --check alias fc := format-assess diff --git a/.rustfmt.toml b/.rustfmt.toml index 10fe2ff..f55bf01 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,12 +1,14 @@ +unstable_features = true match_block_trailing_comma = true max_width = 80 reorder_imports = false reorder_modules = false use_field_init_shorthand = true use_try_shorthand = true +skip_macro_invocations = ["concat"] -# blank_lines_lower_bound = 1 # not stabilized yet +# blank_lines_lower_bound = 1 # where_single_line = true # overflow_delimited_expr = true # normalize_doc_attributes = true From f3a997f29aa68a2307c5e8342bf6cdb7f24208c6 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 14:26:59 -0300 Subject: [PATCH 028/108] Add docs for tables, update roadmap --- static/graph.toml | 78 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/static/graph.toml b/static/graph.toml index fe3a3c7..0527339 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -314,7 +314,7 @@ b c ` -You still get "a b c" as a result. This is the case for paragraphs, but not for lists, verse blocks and preformatted text. Blockquotes support both modes. +You still get "a b c" as a result. This is the case for paragraphs, but not for lists, verse blocks, tables and preformatted text. Blockquotes support both modes. This is useful when editing your text, allowing you to break some thoughts and special syntax without losing control over where your paragraph ends, particularly when handling huge paragraphs. @@ -456,6 +456,50 @@ Lines starting with a `+` character will create numbered lists instead: + ni + san +### Tables + +Tables are blocks delimited by a sole `%` on its own line: + +` +% + Country ! Capital + Colombia | Bogotá + Belgium | Brussels + Palestine | Jerusalem + Zambia | Lusaka +% +` + +% + Country ! Capital + Colombia | Bogotá + Belgium | Brussels + Palestine | Jerusalem + Zambia | Lusaka +% + +Table cells are delimited by either a `!` for headers or `|` for common cells. These delimiters must be surrounded by at least one space to each side and are optional at the first and last position of each line. + +This means you can use any of the following formats: + +` +% + middle | only + tail | only | + | lead | only + | fully | wrapped | +% +` + +% + middle | only + tail | only | + | lead | only + | fully | wrapped | +% + +Because at least one space is required around each delimiter, you must indent the table inside the surrounding `%` markers by at least one space. + ## Rendering unformatted text The backtick character `\\`` can be used to render unformatted blocks and inline text: @@ -482,25 +526,23 @@ Finally, you can precede any character with a `\\\\` to fully _escape_ that char ## Raw HTML -If you need some more advanced feature that is not supported directly by en's markup snytax, you can always just write plain HTML and it will be passed along. For example, you could render a table: +If you need some more advanced feature that is not supported directly by en's markup snytax, you can always just write plain HTML and it will be passed along. For example, you could render a form: ` -<table> - <tr> - <td> Hi, </td> - <td> *HTML*! </td> - </tr> -</table> +<form style="text-align: center;"> + <label for="name"> *__Name__* </label> + <input type="text" id="name"/> + <input type="submit"/> +</form> ` - - - - - -
Hi, *HTML*!
+
+ + + +
-Notice that, as shown in this example, you can mix en syntax and HTML. You might want to add a space between your HTML tags and en special syntax so the boundary is clearer, but otherwise they don't tend to overlap since the symbols most used in HTML are not special in en. +Notice that, as shown in this example, you can mix en syntax and HTML. You might want to add a space between your HTML tags and en special syntax so the boundary is clearer, but otherwise they don't tend to overlap since the symbols most used in HTML are not special in en with the exception of `<`, which is interpreted specially only at the end of lines. If you want to avoid either one of these syntaxes from being interpreted specially, you should escape the relevant characters as explained in the previous section. """ @@ -723,11 +765,7 @@ text = """ - [ ] Invert where redirects are set - [x] Formatting - [x] Blockquotes - - [ ] Tables - - `%` block - - newline for rows - - indented, space-surrounded `!` wrap for headers - - indented, space-surrounded `|` wrap for cells + - [x] Tables - [x] Nested formatting - [x] Headers - [x] Preformatted blocks From 7fa054257a5727f7276223c4efc03fe920fbf076 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 14:41:22 -0300 Subject: [PATCH 029/108] Minor documentation tweaks --- static/graph.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/graph.toml b/static/graph.toml index 0527339..daa7c3d 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -418,7 +418,7 @@ If you have a more complex citation, you can use multiple lines starting with `- -- Benedita da Silva, -- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, -- March 3rd, 1995, < --- International Day for the Elimination of Racial Discrimination. +-- International Day for the Elimination of Racial Discrimination ` > Dois grandes mitos dominam a história oficial do Brasil: @@ -426,7 +426,7 @@ If you have a more complex citation, you can use multiple lines starting with `- -- Benedita da Silva, -- |Speech on the Federal Senate|https://www25.senado.leg.br/web/atividade/pronunciamentos/-/p/pronunciamento/165765|, -- March 3rd, 1995, < --- Dia Internacional para a Eliminação da Discriminação Racial. +-- International Day for the Elimination of Racial Discrimination The first URL found in your citation will be used as the blockquote element's `cite` value. @@ -484,9 +484,9 @@ This means you can use any of the following formats: ` % - middle | only - tail | only | - | lead | only + middle | only + tail | only | + | lead | only | fully | wrapped | % ` From f71ab341a00282ab041912e382fe1c4bf587b391 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 14:43:14 -0300 Subject: [PATCH 030/108] Add graph file to CI trigger paths --- .forgejo/workflows/check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index 18b1d4c..f6b7b75 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -3,6 +3,7 @@ on: paths: - src/** - tests/** + - static/graph.toml - .forgejo/** - Cargo.toml - Cargo.lock From a7770e1486479761496a360b13b9a97708ea8375 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 14:45:32 -0300 Subject: [PATCH 031/108] Add rustfmt from nighly to CI toolchain setup --- .forgejo/workflows/check.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index f6b7b75..0cbf49d 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -28,7 +28,8 @@ jobs: - name: Setup Rust toolchain run: | - rustup component add rustfmt clippy llvm-tools-preview + rustup component add clippy llvm-tools-preview + rustup component add --toolchain nightly rustfmt - name: Setup additional tooling run: | From d7d034757a48926827dc437dd161fc65c3f4369e Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 16:17:10 -0300 Subject: [PATCH 032/108] Adopt and apply nightly rustfmt configuration --- .rustfmt.toml | 52 +++--- src/graph.rs | 28 +-- src/graph/edge.rs | 2 +- src/graph/meta.rs | 19 +- src/graph/node.rs | 6 +- src/lib.rs | 11 +- src/log.rs | 14 +- src/router.rs | 18 +- src/router/handlers/error.rs | 6 +- src/router/handlers/fixed.rs | 11 +- src/router/handlers/graph.rs | 14 +- src/router/handlers/navigation.rs | 6 +- src/router/handlers/raw.rs | 2 +- src/router/handlers/template.rs | 5 +- src/syntax/content.rs | 9 +- src/syntax/content/parser.rs | 30 +-- src/syntax/content/parser/context.rs | 4 +- src/syntax/content/parser/context/anchor.rs | 187 ++++++++++++++----- src/syntax/content/parser/context/block.rs | 14 +- src/syntax/content/parser/context/inline.rs | 5 +- src/syntax/content/parser/context/list.rs | 10 +- src/syntax/content/parser/context/table.rs | 19 +- src/syntax/content/parser/lexeme.rs | 26 +-- src/syntax/content/parser/lexer.rs | 9 +- src/syntax/content/parser/point.rs | 26 ++- src/syntax/content/parser/segment.rs | 13 +- src/syntax/content/parser/state.rs | 13 +- src/syntax/content/parser/token.rs | 25 ++- src/syntax/content/parser/token/anchor.rs | 65 +++---- src/syntax/content/parser/token/bold.rs | 23 +-- src/syntax/content/parser/token/checkbox.rs | 15 +- src/syntax/content/parser/token/code.rs | 23 +-- src/syntax/content/parser/token/header.rs | 14 +- src/syntax/content/parser/token/item.rs | 13 +- src/syntax/content/parser/token/linebreak.rs | 19 +- src/syntax/content/parser/token/list.rs | 7 +- src/syntax/content/parser/token/literal.rs | 11 +- src/syntax/content/parser/token/oblique.rs | 23 +-- src/syntax/content/parser/token/paragraph.rs | 20 +- src/syntax/content/parser/token/preformat.rs | 24 +-- src/syntax/content/parser/token/quote.rs | 8 +- src/syntax/content/parser/token/strike.rs | 19 +- src/syntax/content/parser/token/table.rs | 16 +- src/syntax/content/parser/token/underline.rs | 19 +- src/syntax/content/parser/token/verse.rs | 4 +- 45 files changed, 432 insertions(+), 475 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index f55bf01..a09015c 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,27 +1,33 @@ unstable_features = true -match_block_trailing_comma = true max_width = 80 -reorder_imports = false -reorder_modules = false -use_field_init_shorthand = true -use_try_shorthand = true +inline_attribute_width = 40 + skip_macro_invocations = ["concat"] -# not stabilized yet -# blank_lines_lower_bound = 1 -# where_single_line = true -# overflow_delimited_expr = true -# normalize_doc_attributes = true -# normalize_comments = true -# inline_attribute_width = 40 -# imports_granularity = "Crate" -# hex_literal_case = "Lower" -# group_imports = "StdExternalCrate" -# format_strings = true -# force_multiline_blocks = true -# error_on_unformatted = true -# error_on_line_overflow = true -# condense_wildcard_suffixes = true -# doc_comment_code_block_width = 70 -# format_code_in_doc_comments = true -# wrap_comments = true +imports_granularity = "Crate" +group_imports = "StdExternalCrate" + +fn_single_line = true +match_block_trailing_comma = true +use_field_init_shorthand = true +use_try_shorthand = true +hex_literal_case = "Lower" +where_single_line = true +condense_wildcard_suffixes = true +combine_control_expr = false +empty_item_single_line = true +reorder_impl_items = true +trailing_semicolon = false + +wrap_comments = true +normalize_comments = true +normalize_doc_attributes = true +format_code_in_doc_comments = true +doc_comment_code_block_width = 70 + +error_on_unformatted = true +error_on_line_overflow = true + +ignore = [ + "tests/mocks", +] diff --git a/src/graph.rs b/src/graph.rs index 709859f..ca64d65 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,24 +1,24 @@ use std::{collections::HashMap, path::PathBuf}; -use serde::{Serialize, Deserialize}; +pub use edge::Edge; +pub use meta::{Config, Meta}; +pub use node::Node; +use serde::{Deserialize, Serialize}; -use crate::syntax::{ - command::Arguments, - content::{ - self, - parser::{flatten, Token, token::Anchor}, +use crate::{ + prelude::*, + syntax::{ + command::Arguments, + content::{ + self, + parser::{Token, flatten, token::Anchor}, + }, }, }; -use crate::prelude::*; -pub use { - node::Node, - edge::Edge, - meta::{Meta, Config}, -}; -pub mod node; pub mod edge; pub mod meta; +pub mod node; #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] pub struct Graph { @@ -399,7 +399,7 @@ impl Graph { } else { log!( VERBOSE, - "Chasing candidate for query {query}, collapsed {collapsed_query}" + "Chasing candidate: query {query}, collapsed {collapsed_query}" ); } diff --git a/src/graph/edge.rs b/src/graph/edge.rs index 02961ac..1c34bca 100644 --- a/src/graph/edge.rs +++ b/src/graph/edge.rs @@ -1,4 +1,4 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] pub struct Edge { diff --git a/src/graph/meta.rs b/src/graph/meta.rs index 56664e8..f3669fb 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -1,6 +1,6 @@ -use crate::prelude::*; +use serde::{Deserialize, Serialize}; -use serde::{Serialize, Deserialize}; +use crate::prelude::*; #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct Meta { @@ -105,15 +105,9 @@ impl Default for Config { } // See: https://github.com/serde-rs/serde/issues/368 -fn mktrue() -> bool { - true -} -fn mkfalse() -> bool { - false -} -fn mk8() -> u16 { - 8 -} +fn mktrue() -> bool { true } +fn mkfalse() -> bool { false } +fn mk8() -> u16 { 8 } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct Version { @@ -314,9 +308,8 @@ impl std::fmt::Display for VersionErrorCause { #[cfg(test)] mod tests { - use crate::graph::Graph; - use super::*; + use crate::graph::Graph; #[test] fn empty_footer_text() { diff --git a/src/graph/node.rs b/src/graph/node.rs index 672f835..ff0c719 100644 --- a/src/graph/node.rs +++ b/src/graph/node.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use super::edge::Edge; @@ -118,7 +118,9 @@ mod tests { assert_eq!( format!("{node}"), format!( - "Node 404 [title:'Not Found' text:15l summary:{} redirect:{redirect}]", + "Node 404 [title:'Not Found' \ + text:15l summary:{} \ + redirect:{redirect}]", summary.len(), ) ); diff --git a/src/lib.rs b/src/lib.rs index 2a71d03..2cbd04c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,17 @@ use std::{sync, time}; pub mod prelude { - pub use crate::log; - pub use crate::tlog; - pub use crate::log::Level::*; - pub use crate::log::now; + pub use crate::{ + log, + log::{Level::*, now}, + tlog, + }; } pub mod graph; +pub mod log; pub mod router; pub mod syntax; -pub mod log; pub static ONSET: sync::LazyLock = sync::LazyLock::new(time::Instant::now); diff --git a/src/log.rs b/src/log.rs index 4f1c85e..a079d1e 100644 --- a/src/log.rs +++ b/src/log.rs @@ -32,7 +32,7 @@ impl Data { let path = make_display_path(captured_path, &env_level); let is_silent = env_level <= Level::SILENT; - let message_level_is_within_env_level = message_level <= env_level; + let level_within_env_level = message_level <= env_level; let excluded_in_code = !EXCLUSIONS.iter().all(|&s| !trace_string.contains(s)); let excluded_by_env = @@ -41,7 +41,7 @@ impl Data { filter.is_empty() || captured_path.contains(&filter); let should_log = !is_silent - && message_level_is_within_env_level + && level_within_env_level && !excluded_in_code && !excluded_by_env && matches_filter; @@ -51,7 +51,7 @@ impl Data { eprintln!( "Log decision for message from {path}: {should_log} given\n\ is_silent: {is_silent} (expected false)\n\ - message_level_is_within_env_level: {message_level_is_within_env_level}\n\ + level_within_env_level: {level_within_env_level}\n\ excluded_in_code: {excluded_in_code} (expected false)\n\ excluded_by_env: {excluded_by_env} (expected false)\n\ matches_filter: {matches_filter}\n\ @@ -118,9 +118,7 @@ macro_rules! tlog { }}; } -pub fn now() -> Instant { - Instant::now() -} +pub fn now() -> Instant { Instant::now() } #[allow(clippy::print_stderr)] pub fn elog(function: &str, message: &str) { @@ -211,9 +209,7 @@ pub fn wrap(s: &str) -> String { } } - fn escape(s: &str) -> String { - s.escape_debug().collect() - } + fn escape(s: &str) -> String { s.escape_debug().collect() } symbolize("e(&escape(s))) } diff --git a/src/router.rs b/src/router.rs index 4617e47..791faa9 100644 --- a/src/router.rs +++ b/src/router.rs @@ -3,13 +3,13 @@ use axum::{Router, routing::get}; use crate::graph::Graph; mod handlers { - pub mod graph; - pub mod template; - pub mod raw; - pub mod navigation; - pub mod fixed; pub mod error; + pub mod fixed; + pub mod graph; pub mod mime; + pub mod navigation; + pub mod raw; + pub mod template; } #[derive(Clone)] @@ -49,11 +49,6 @@ pub fn new(graph: Graph) -> Router { #[cfg(test)] mod tests { - use crate::{ - graph::{Graph, Config, Meta}, - }; - - use super::*; use axum::{ body::Body, http::{Request, StatusCode}, @@ -61,6 +56,9 @@ mod tests { }; use tower::ServiceExt as _; + use super::*; + use crate::graph::{Config, Graph, Meta}; + async fn request(uri: &str, config: Option<&Config>) -> Response { let default_graph = Graph::load(); let graph = Graph { diff --git a/src/router/handlers/error.rs b/src/router/handlers/error.rs index 5367f8b..fdb938d 100644 --- a/src/router/handlers/error.rs +++ b/src/router/handlers/error.rs @@ -68,10 +68,8 @@ pub async fn not_found(State(state): State) -> Response { #[cfg(test)] mod tests { - use axum::{ - http::{StatusCode}, - extract::State, - }; + use axum::{extract::State, http::StatusCode}; + use super::*; #[tokio::test] diff --git a/src/router/handlers/fixed.rs b/src/router/handlers/fixed.rs index 67a764a..ac66951 100644 --- a/src/router/handlers/fixed.rs +++ b/src/router/handlers/fixed.rs @@ -1,16 +1,13 @@ use axum::{ + body::Body, + extract::{Path, State}, http::{HeaderValue, Response, StatusCode, header}, - { - body::Body, - extract::{Path, State}, - }, }; -use crate::prelude::*; use crate::{ graph::{Format, Graph, SerialErrorCause}, - router::{GlobalState, handlers}, - router::handlers::mime::Mime, + prelude::*, + router::{GlobalState, handlers, handlers::mime::Mime}, }; pub async fn file( diff --git a/src/router/handlers/graph.rs b/src/router/handlers/graph.rs index a1912c7..349efb7 100644 --- a/src/router/handlers/graph.rs +++ b/src/router/handlers/graph.rs @@ -1,7 +1,8 @@ use axum::{ - extract::State, - response::IntoResponse as _, - {body::Body, extract::Path, http::Response, response::Redirect}, + body::Body, + extract::{Path, State}, + http::Response, + response::{IntoResponse as _, Redirect}, }; use crate::{ @@ -49,13 +50,10 @@ pub async fn node( #[cfg(test)] mod tests { - use axum::{ - http::{HeaderName, StatusCode}, - }; - - use crate::graph::{Format, Graph}; + use axum::http::{HeaderName, StatusCode}; use super::*; + use crate::graph::{Format, Graph}; async fn wrap_node(query: &str) -> Response { let state = GlobalState { diff --git a/src/router/handlers/navigation.rs b/src/router/handlers/navigation.rs index 12bd617..f799416 100644 --- a/src/router/handlers/navigation.rs +++ b/src/router/handlers/navigation.rs @@ -1,4 +1,6 @@ -use axum::{Form, body::Body, extract::State, http::Response, response::Redirect}; +use axum::{ + Form, body::Body, extract::State, http::Response, response::Redirect, +}; use crate::{ prelude::*, @@ -58,9 +60,9 @@ pub struct Query { #[cfg(test)] mod tests { use axum::http::StatusCode; - use crate::graph::Graph; use super::*; + use crate::graph::Graph; async fn wrap_page(path: &str) -> Response { let state = GlobalState { diff --git a/src/router/handlers/raw.rs b/src/router/handlers/raw.rs index 789e4c2..a593cbe 100644 --- a/src/router/handlers/raw.rs +++ b/src/router/handlers/raw.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - http::{header, HeaderValue, Response, StatusCode}, + http::{HeaderValue, Response, StatusCode, header}, }; use crate::prelude::*; diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index c590424..a2290d5 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - http::{header, Response, StatusCode}, + http::{Response, StatusCode, header}, }; use crate::{ @@ -135,9 +135,8 @@ fn emergency_wrap(error: &tera::Error) -> String { #[cfg(test)] mod tests { - use crate::graph::Graph; - use super::*; + use crate::graph::Graph; #[test] fn by_filename_forced_error() { diff --git a/src/syntax/content.rs b/src/syntax/content.rs index 0cd700b..29ef886 100644 --- a/src/syntax/content.rs +++ b/src/syntax/content.rs @@ -1,6 +1,6 @@ use std::mem::discriminant; -use parser::{Token, Lexeme}; +use parser::{Lexeme, Token}; use crate::graph::Graph; @@ -43,9 +43,7 @@ impl TokenOutput { } } -pub fn parse(text: &str, graph: &Graph) -> String { - parser::read(text, graph) -} +pub fn parse(text: &str, graph: &Graph) -> String { parser::read(text, graph) } pub fn rich_parse(text: &str, graph: &Graph) -> TokenOutput { parser::rich_read(text, graph) @@ -53,9 +51,8 @@ pub fn rich_parse(text: &str, graph: &Graph) -> TokenOutput { #[cfg(test)] mod tests { - use crate::syntax::content::parser::token::{Bold, Oblique}; - use super::*; + use crate::syntax::content::parser::token::{Bold, Oblique}; #[test] fn only() { diff --git a/src/syntax/content/parser.rs b/src/syntax/content/parser.rs index 0a3c0f0..2640edf 100644 --- a/src/syntax/content/parser.rs +++ b/src/syntax/content/parser.rs @@ -1,15 +1,18 @@ -use crate::{prelude::*, graph::Graph, syntax::content::TokenOutput}; use context::{Block, Inline}; +pub use lexeme::Lexeme; use lexer::{LEXMAP, lex}; -pub use {lexeme::Lexeme, token::Token, state::State}; +pub use state::State; +pub use token::Token; + +use crate::{graph::Graph, prelude::*, syntax::content::TokenOutput}; -pub mod token; -pub mod lexer; -pub mod lexeme; -pub mod segment; pub mod context; +pub mod lexeme; +pub mod lexer; pub mod point; +pub mod segment; pub mod state; +pub mod token; fn parse(tokens: &[Token]) -> String { tokens.iter().map(Token::render).collect::() @@ -46,16 +49,10 @@ pub fn flatten(input: &str, graph: &Graph) -> String { #[cfg(test)] mod tests { - use crate::{ - graph::Graph, - syntax::content::parser::{token::header::Level}, - }; - use super::*; + use crate::{graph::Graph, syntax::content::parser::token::header::Level}; - fn read_noconfig(input: &str) -> String { - read(input, &Graph::default()) - } + fn read_noconfig(input: &str) -> String { read(input, &Graph::default()) } #[test] fn empty_render_is_empty() { @@ -65,7 +62,10 @@ mod tests { #[test] fn mixed_sample() { let en = "`this |test|` tries ## to |brea|k|: things"; - let html = r#"

this |test| tries ## to brea: things

"#; + let html = concat!( + r#"

this |test| tries ## to brea: things

"#, + ); assert_eq!(read_noconfig(en), html); } diff --git a/src/syntax/content/parser/context.rs b/src/syntax/content/parser/context.rs index 861b9c1..6320a5d 100644 --- a/src/syntax/content/parser/context.rs +++ b/src/syntax/content/parser/context.rs @@ -3,9 +3,9 @@ use crate::syntax::content::parser::{ token::{Header, Paragraph, PreFormat, Verse}, }; +pub mod anchor; pub mod block; pub mod inline; -pub mod anchor; pub mod list; pub mod quote; pub mod table; @@ -68,7 +68,7 @@ pub fn close(state: &State, tokens: &mut Vec) { #[cfg(test)] mod tests { - use crate::syntax::content::parser::{context::Block, State}; + use crate::syntax::content::parser::{State, context::Block}; #[test] #[should_panic(expected = "End of input with open list")] diff --git a/src/syntax/content/parser/context/anchor.rs b/src/syntax/content/parser/context/anchor.rs index d9a861a..1407385 100644 --- a/src/syntax/content/parser/context/anchor.rs +++ b/src/syntax/content/parser/context/anchor.rs @@ -1,7 +1,7 @@ use crate::{ - prelude::*, - syntax::content::parser::{context::Inline, Lexeme, State, Token}, graph::Graph, + prelude::*, + syntax::content::parser::{Lexeme, State, Token, context::Inline}, }; /// Handles open anchor contexts until an anchor token is fully parsed. @@ -162,11 +162,9 @@ fn push( #[cfg(test)] mod tests { - use crate::{syntax::content::parser, graph::Graph}; + use crate::{graph::Graph, syntax::content::parser}; - fn read(input: &str) -> String { - parser::read(input, &Graph::default()) - } + fn read(input: &str) -> String { parser::read(input, &Graph::default()) } #[test] fn flanking() { @@ -188,7 +186,10 @@ mod tests { fn flanking_with_trailing_comma_and_space() { assert_eq!( read("|Node|, at"), - r#"

Node, at

"# + concat!( + r#"

Node, at

"#, + ) ); } @@ -204,7 +205,8 @@ mod tests { fn needless_three_pipe_anchor() { assert_eq!( read("|Node|Destination|"), - r#"

Node

"# + concat!(r#"

Node

"#) ); } @@ -212,7 +214,10 @@ mod tests { fn nonleading_second_pipe() { assert_eq!( read("Go to Node|Destination|, here"), - r#"

Go to Node, here

"#, + concat!( + r#"

Go to Node, here

"# + ), ); } @@ -220,7 +225,9 @@ mod tests { fn anchor_to_node_s() { assert_eq!( read("The |letter s|s|'s node: |s|!"), - r#"

The letter s's node: s!

"# + concat!(r#"

The letter s's node: "#, + r#"s!

"#) ); } @@ -228,7 +235,10 @@ mod tests { fn nonleading_plural_anchor() { assert_eq!( read("The flower|s bloomed"), - r#"

The flowers bloomed

"# + concat!( + r#"

The flowers bloomed

"#, + ) ); } @@ -236,7 +246,9 @@ mod tests { fn leading_plural_anchor() { assert_eq!( read("Interfaces are |element|s of |system|s."), - r#"

Interfaces are elements of systems.

"# + concat!(r#"

Interfaces are elements of systems.

"#) ); } @@ -244,7 +256,11 @@ mod tests { fn leading_multiword_anchor() { assert_eq!( read("interactions are |basic elements| of systems"), - r#"

interactions are basic elements of systems

"# + concat!( + r#"

interactions are basic elements "#, + r#"of systems

"#, + ), ); } @@ -252,7 +268,9 @@ mod tests { fn explicit_end_of_destination() { assert_eq!( read("interactions are |basic elements|BasicElements| of systems"), - r#"

interactions are basic elements of systems

"# + concat!(r#"

interactions are basic elements of "#, + r#"systems

"#) ); } @@ -260,7 +278,11 @@ mod tests { fn explicit_end_of_external_destination() { assert_eq!( read("this |anchor example|https://example.com| is external"), - r#"

this anchor example is external

"# + concat!( + r#"

this anchor example is "#, + r#"external

"# + ) ); } @@ -276,7 +298,10 @@ mod tests { fn external_anchor_destination_at_eoi() { assert_eq!( read("a b|https://example.com"), - r#"

a b

"# + concat!( + r#"

a b

"#, + ) ); } @@ -284,7 +309,10 @@ mod tests { fn nonleading_plural_anchor_at_eoi() { assert_eq!( read("element|s"), - r#"

elements

"# + concat!( + r#"

elements

"#, + ) ); } @@ -292,17 +320,22 @@ mod tests { fn leading_plural_anchor_at_eoi() { assert_eq!( read("|element|s"), - r#"

elements

"# + concat!( + r#"

elements

"#, + ) ); } #[test] fn http_external_anchor() { assert_eq!( - read( - "a |false dichotomy|https://en.wikipedia.org/wiki/False_dilemma|." + read("a |false dichotomy|https://wikipedia.org/False_dilemma|."), + concat!( + r#"

a "#, + r#"false dichotomy.

"#, ), - r#"

a false dichotomy.

"# ); } @@ -315,7 +348,8 @@ mod tests { "at rustup.rs", )), concat!( - r#"

Rust toolchain"#, + r#"

Rust toolchain"#, "\n", "at rustup.rs

", ) @@ -326,7 +360,11 @@ mod tests { fn http_external_anchor_leading_no_third_then_space() { assert_eq!( read("|Rust toolchain|https://rustup.rs/ at rustup.rs"), - r#"

Rust toolchain at rustup.rs

"# + concat!( + r#"

Rust toolchain "#, + r#"at rustup.rs

"#, + ), ); } @@ -334,7 +372,10 @@ mod tests { fn http_external_anchor_leading_no_third_then_eoi() { assert_eq!( read("|Rust toolchain|https://rustup.rs/"), - r#"

Rust toolchain

"# + concat!( + r#"

Rust toolchain

"#, + ) ); } @@ -344,7 +385,10 @@ mod tests { read("\n|SomeAnchor|\n"), concat!( "\n", - r#"

SomeAnchor

"# + concat!( + r#"

SomeAnchor

"#, + ) ), ); } @@ -354,9 +398,11 @@ mod tests { assert_eq!( read("|SomeAnchor|\n|SomeOtherAnchor|\n"), concat!( - r#"

SomeAnchor"#, + r#"

SomeAnchor"#, "\n", - r#"SomeOtherAnchor

"# + r#"SomeOtherAnchor

"#, ) ); } @@ -366,10 +412,12 @@ mod tests { assert_eq!( read("|SomeAnchor|\n\n|SomeOtherAnchor|\n"), concat!( - r#"

SomeAnchor

"#, + r#"

SomeAnchor

"#, "\n", "\n", - r#"

SomeOtherAnchor

"# + r#"

SomeOtherAnchor

"#, ), ); } @@ -378,7 +426,10 @@ mod tests { fn trailing_anchor() { assert_eq!( read("see acks|acks"), - r#"

see acks

"# + concat!( + r#"

see acks

"#, + ) ); } @@ -388,8 +439,9 @@ mod tests { read("\nsee acks|acks\n"), concat!( "\n", - r#"

see acks

"# - ) + r#"

see acks

"#, + ), ); } @@ -417,7 +469,10 @@ mod tests { fn anchor_with_trailing_single_quote() { assert_eq!( read("the |lion|'s mouth"), - r#"

the lion's mouth

"#, + concat!( + r#"

the lion's mouth

"#, + ) ); } @@ -425,7 +480,10 @@ mod tests { fn anchor_with_trailing_double_quote() { assert_eq!( read(r#"the "|real|" motive"#), - r#"

the "real" motive

"#, + concat!( + r#"

the "real" motive

"#, + ) ); } @@ -433,7 +491,10 @@ mod tests { fn anchor_with_trailing_parenthesis() { assert_eq!( read("this (though |true|) was questioned"), - r#"

this (though true) was questioned

"#, + concat!( + r#"

this (though true) was questioned

"#, + ) ); } @@ -441,7 +502,10 @@ mod tests { fn anchor_with_leading_single_quote() { assert_eq!( read("the 'real|Reality' motive"), - r#"

the 'real' motive

"#, + concat!( + r#"

the 'real' motive

"#, + ) ); } @@ -449,7 +513,10 @@ mod tests { fn anchor_with_leading_double_quote() { assert_eq!( read(r#"the "real|Reality" motive"#), - r#"

the "real" motive

"#, + concat!( + r#"

the "real" motive

"#, + ) ); } @@ -457,7 +524,10 @@ mod tests { fn anchor_with_leading_parenthesis() { assert_eq!( read("her (last|Surname) name"), - r#"

her (last) name

"#, + concat!( + r#"

her (last) name

"#, + ) ); } @@ -465,7 +535,10 @@ mod tests { fn anchor_with_internal_apostrophe() { assert_eq!( read("the |lion's mouth|album was released"), - r#"

the lion's mouth was released

"# + concat!( + r#"

the lion's mouth was released

"#, + ) ); } @@ -473,7 +546,10 @@ mod tests { fn nonleading_anchor_with_internal_apostrophe() { assert_eq!( read("they decided to stay at Jane's|YellowHouse that night"), - r#"

they decided to stay at Jane's that night

"# + concat!( + r#"

they decided to stay at Jane's that night

"#, + ) ); } @@ -481,7 +557,10 @@ mod tests { fn nonleading_anchor_with_internal_apostrophe_at_eoi() { assert_eq!( read("they decided to stay at Jane's|YellowHouse"), - r#"

they decided to stay at Jane's

"# + concat!( + r#"

they decided to stay at Jane's

"#, + ) ); } @@ -489,7 +568,10 @@ mod tests { fn nonleading_anchor_with_internal_apostrophe_at_soi() { assert_eq!( read("Jane's|YellowHouse that night"), - r#"

Jane's that night

"# + concat!( + r#"

Jane's that night

"#, + ) ); } @@ -497,7 +579,10 @@ mod tests { fn anchor_with_internal_double_quotes() { assert_eq!( read(r#"the |"real"|Truth motive"#), - r#"

the "real" motive

"#, + concat!( + r#"

the "real" motive

"#, + ) ); } @@ -505,7 +590,10 @@ mod tests { fn anchor_with_internal_double_quotes_wrapping_spaced_words() { assert_eq!( read(r#"the |"bare reality"|Ideology they believed"#), - r#"

the "bare reality" they believed

"#, + concat!( + r#"

the "bare reality" they believed

"#, + ) ); } @@ -513,7 +601,10 @@ mod tests { fn anchor_with_internal_parenthesis() { assert_eq!( read("her |last (name)|Surname was Amad"), - r#"

her last (name) was Amad

"#, + concat!( + r#"

her last (name) was Amad

"#, + ) ); } @@ -521,7 +612,11 @@ mod tests { fn anchor_with_internal_parenthesis_wrapping_spaced_words() { assert_eq!( read("this |truth (though questionable) was fine|Absurd to them "), - r#"

this truth (though questionable) was fine to them

"# + concat!( + r#"

this truth (though questionable) was "#, + r#"fine to them

"#, + ) ); } } diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index 292bdf0..63a3ac9 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -8,7 +8,7 @@ use crate::{ parser::{ Block, Lexeme, State, Token, token::{ - Header, List, LineBreak, Literal, Paragraph, PreFormat, Quote, + Header, LineBreak, List, Literal, Paragraph, PreFormat, Quote, Table, Verse, }, }, @@ -124,16 +124,14 @@ pub fn parse( mod tests { use crate::{ - syntax::content::parser::{ - self, Block, Token, context, State, - token::{Header, header::Level, PreFormat}, - }, graph::Graph, + syntax::content::parser::{ + self, Block, State, Token, context, + token::{Header, PreFormat, header::Level}, + }, }; - fn read(input: &str) -> String { - parser::read(input, &Graph::default()) - } + fn read(input: &str) -> String { parser::read(input, &Graph::default()) } #[test] fn pre() { diff --git a/src/syntax/content/parser/context/inline.rs b/src/syntax/content/parser/context/inline.rs index 27c4675..b4412cc 100644 --- a/src/syntax/content/parser/context/inline.rs +++ b/src/syntax/content/parser/context/inline.rs @@ -1,17 +1,16 @@ use std::{iter::Peekable, slice::Iter}; use crate::{ + graph::Graph, prelude::*, syntax::content::{ Parseable as _, parser::{ - Lexeme, State, + Inline, Lexeme, State, Token, context, state::AnchorBuffer, - Inline, context, Token, token::{Anchor, Code, Literal}, }, }, - graph::Graph, }; pub fn parse( diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index 1fc938a..7c72103 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -1,11 +1,11 @@ use std::{iter::Peekable, slice::Iter}; use crate::{ + graph::Graph, prelude::*, syntax::content::parser::{ - context::Block, Token, Lexeme, State, state, token::Item, format, + Lexeme, State, Token, context::Block, format, state, token::Item, }, - graph::Graph, }; /// Handles open list contexts until a list is fully parsed. @@ -88,13 +88,11 @@ pub fn parse( #[cfg(test)] mod tests { use crate::{ - syntax::content::parser::{self, context::list::parse, Lexeme, State}, graph::Graph, + syntax::content::parser::{self, Lexeme, State, context::list::parse}, }; - fn read(input: &str) -> String { - parser::read(input, &Graph::default()) - } + fn read(input: &str) -> String { parser::read(input, &Graph::default()) } #[test] fn unordered_list() { diff --git a/src/syntax/content/parser/context/table.rs b/src/syntax/content/parser/context/table.rs index 357f599..a64d136 100644 --- a/src/syntax/content/parser/context/table.rs +++ b/src/syntax/content/parser/context/table.rs @@ -117,11 +117,9 @@ pub fn parse( #[cfg(test)] mod tests { - use crate::{syntax::content::parser, graph::Graph}; + use crate::{graph::Graph, syntax::content::parser}; - fn read(input: &str) -> String { - parser::read(input, &Graph::default()) - } + fn read(input: &str) -> String { parser::read(input, &Graph::default()) } fn read_loaded(input: &str) -> String { parser::read(input, &Graph::load()) @@ -250,11 +248,13 @@ mod tests { "", "\n", " ", "\n", " ", "\n", - r#" "#, "\n", + r#" "#, "\n", " ", "\n", " ", "\n", "
aNodeNodec
", "\n", - )); + ) + ); } #[test] @@ -270,11 +270,14 @@ mod tests { "", "\n", " ", "\n", " ", "\n", - r#" "#, "\n", + r#" "#, "\n", " ", "\n", " ", "\n", "
aNodeNodec
", "\n", - )); + ) + ); } #[test] diff --git a/src/syntax/content/parser/lexeme.rs b/src/syntax/content/parser/lexeme.rs index acaf378..b361fb1 100644 --- a/src/syntax/content/parser/lexeme.rs +++ b/src/syntax/content/parser/lexeme.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::{syntax::content::parser::segment::delimiter::Delimiters}; +use crate::syntax::content::parser::segment::delimiter::Delimiters; #[derive(Clone, Debug, Default)] pub struct Lexeme { @@ -22,25 +22,15 @@ impl Lexeme { } } - pub fn text(&self) -> String { - self.text.clone() - } + pub fn text(&self) -> String { self.text.clone() } - pub fn next(&self) -> String { - self.next.clone() - } + pub fn next(&self) -> String { self.next.clone() } - pub fn last(&self) -> bool { - self.last - } + pub fn last(&self) -> bool { self.last } - pub fn first(&self) -> bool { - self.first - } + pub fn first(&self) -> bool { self.first } - pub fn mutate_text(&mut self, new: &str) { - self.text = new.to_string(); - } + pub fn mutate_text(&mut self, new: &str) { self.text = new.to_string(); } pub fn as_char(&self) -> Option { if self.text.chars().count() == 1 { @@ -141,9 +131,7 @@ impl Lexeme { .is_some_and(|c| delimiters.is_delimiter(c)) } - pub fn next_first_char(&self) -> Option { - self.next.chars().nth(0) - } + pub fn next_first_char(&self) -> Option { self.next.chars().nth(0) } pub fn match_first_char(&self, query: char) -> bool { self.text.chars().nth(0).is_some_and(|c| c == query) diff --git a/src/syntax/content/parser/lexer.rs b/src/syntax/content/parser/lexer.rs index b87d868..e9a0d40 100644 --- a/src/syntax/content/parser/lexer.rs +++ b/src/syntax/content/parser/lexer.rs @@ -1,13 +1,14 @@ use crate::{ - prelude::*, graph::Graph, + prelude::*, syntax::content::{ - TokenOutput, Parseable as _, LexMap, + LexMap, Parseable as _, TokenOutput, parser::{ + context, lexeme::Lexeme, - token::{Token, LineBreak, Literal}, + point, segment, state::State, - segment, context, point, + token::{LineBreak, Literal, Token}, }, }, }; diff --git a/src/syntax/content/parser/point.rs b/src/syntax/content/parser/point.rs index 38aaec0..b97b2e1 100644 --- a/src/syntax/content/parser/point.rs +++ b/src/syntax/content/parser/point.rs @@ -58,17 +58,18 @@ pub fn parse( #[cfg(test)] mod tests { - use crate::{syntax::content::parser, graph::Graph}; + use crate::{graph::Graph, syntax::content::parser}; - fn read(input: &str) -> String { - parser::read(input, &Graph::default()) - } + fn read(input: &str) -> String { parser::read(input, &Graph::default()) } #[test] fn oblique_anchor() { assert_eq!( read("w _|S|_ w"), - r#"

w S w

"# + concat!( + r#"

w S w

"#, + ) ); } @@ -76,7 +77,8 @@ mod tests { fn oblique_anchor_with_trailing_comma() { assert_eq!( read("w _|S|_, w"), - r#"

w S, w

"# + concat!(r#"

w S, w

"#) ); } @@ -84,9 +86,17 @@ mod tests { fn oblique() { assert_eq!( read( - "_|this anchor is oblique|o as are these literals_ but not these _just these_, not this _and these with an |anc80r| again_" + "_|this anchor is oblique|o as are these literals_ but not \ + these _just these_, not this _and these with an |anc80r| \ + again_" ), - r#"

this anchor is oblique as are these literals but not these just these, not this and these with an anc80r again

"# + concat!( + r#"

"#, + r#"this anchor is oblique as are these literals "#, + r#"but not these just these, not this and these "#, + r#"with an anc80r again

"#, + ) ); } diff --git a/src/syntax/content/parser/segment.rs b/src/syntax/content/parser/segment.rs index 8a8ea76..1ee853e 100644 --- a/src/syntax/content/parser/segment.rs +++ b/src/syntax/content/parser/segment.rs @@ -1,6 +1,4 @@ -pub fn segment(text: &str) -> Vec { - delimiter::atomize(text) -} +pub fn segment(text: &str) -> Vec { delimiter::atomize(text) } pub mod delimiter { @@ -146,7 +144,8 @@ pub mod delimiter { fn atomize_flankign_sentence() { assert_eq!( atomize( - "about_colors: the colors _amber_, _orange_ and _yellow mustard_ to `jane_bishop@mail.com`." + "about_colors: the colors _amber_, _orange_ and \ + _yellow mustard_ to `jane_bishop@mail.com`." ), vec![ "about_colors", @@ -188,7 +187,8 @@ pub mod delimiter { #[test] fn atomize_words() { let actual = atomize( - " justification for the actions of those who hold authority inevitably dwindles ", + " justification for the actions of those who hold \ + authority inevitably dwindles ", ); let expected = vec![ " ", @@ -285,7 +285,8 @@ pub mod delimiter { #[test] fn atomize_pipes_and_ticks() { let actual = atomize( - "every other |time| as `it could or |perhaps somehow|then or now| it was` perceived", + "every other |time| as `it could or |perhaps somehow|then or \ + now| it was` perceived", ); let expected = vec![ "every", diff --git a/src/syntax/content/parser/state.rs b/src/syntax/content/parser/state.rs index cea6f6f..aa42f68 100644 --- a/src/syntax/content/parser/state.rs +++ b/src/syntax/content/parser/state.rs @@ -101,24 +101,24 @@ mod tests { #[test] fn anchor_buffer_display_with_text_set() { let mut buffer = AnchorBuffer::default(); - buffer.text = String::from("mX8Z7yWmsK"); + buffer.text = String::from("mX8Z7sK"); println!("{buffer:#?}"); println!("{buffer}"); assert_eq!( format!("{buffer}"), - r#"AnchorBuffer [text: "mX8Z7yWmsK"] >> Anchor -> "# + r#"AnchorBuffer [text: "mX8Z7sK"] >> Anchor -> "# ); } #[test] fn anchor_buffer_display_with_destination_set() { let mut buffer = AnchorBuffer::default(); - buffer.destination = String::from("VP2aqGngAq"); + buffer.destination = String::from("VP2gAq"); println!("{buffer:#?}"); println!("{buffer}"); assert_eq!( format!("{buffer}"), - r#"AnchorBuffer [, dest: "VP2aqGngAq"] >> Anchor -> "# + r#"AnchorBuffer [, dest: "VP2gAq"] >> Anchor -> "# ); } @@ -131,7 +131,10 @@ mod tests { println!("{buffer}"); assert_eq!( format!("{buffer}"), - r#"AnchorBuffer [text: "ECJrzgkBHg", dest: "9dy6gQ2g3E"] >> Anchor -> "# + concat!( + r#"AnchorBuffer [text: "ECJrzgkBHg", dest: "9dy6gQ2g3E"] "#, + r#">> Anchor -> "#, + ) ); } } diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index cfbb39b..a1f9a70 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -1,4 +1,4 @@ -use crate::syntax::content::{Parseable as _}; +use crate::syntax::content::Parseable as _; pub mod anchor; pub mod bold; @@ -18,12 +18,23 @@ pub mod table; pub mod underline; pub mod verse; -pub use { - anchor::Anchor, bold::Bold, checkbox::CheckBox, code::Code, header::Header, - item::Item, linebreak::LineBreak, list::List, literal::Literal, - oblique::Oblique, paragraph::Paragraph, preformat::PreFormat, quote::Quote, - strike::Strike, table::Table, underline::Underline, verse::Verse, -}; +pub use anchor::Anchor; +pub use bold::Bold; +pub use checkbox::CheckBox; +pub use code::Code; +pub use header::Header; +pub use item::Item; +pub use linebreak::LineBreak; +pub use list::List; +pub use literal::Literal; +pub use oblique::Oblique; +pub use paragraph::Paragraph; +pub use preformat::PreFormat; +pub use quote::Quote; +pub use strike::Strike; +pub use table::Table; +pub use underline::Underline; +pub use verse::Verse; #[derive(Debug, Eq, PartialEq, Clone)] pub enum Token { diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs index 0eb2570..644dc2e 100644 --- a/src/syntax/content/parser/token/anchor.rs +++ b/src/syntax/content/parser/token/anchor.rs @@ -1,6 +1,6 @@ use crate::{ - syntax::content::{Parseable, parser::Lexeme}, graph::Node, + syntax::content::{Parseable, parser::Lexeme}, }; #[derive(Default, Debug, Clone, Eq, PartialEq)] @@ -15,58 +15,36 @@ pub struct Anchor { } impl Anchor { - pub fn text(&self) -> String { - self.text.clone() - } + pub fn text(&self) -> String { self.text.clone() } - pub fn set_text(&mut self, text: &str) { - self.text = String::from(text); - } + pub fn set_text(&mut self, text: &str) { self.text = String::from(text); } - pub fn text_push(&mut self, text: &str) { - self.text.push_str(text); - } + pub fn text_push(&mut self, text: &str) { self.text.push_str(text); } - pub fn destination(&self) -> Option { - self.destination.clone() - } + pub fn destination(&self) -> Option { self.destination.clone() } pub fn set_destination(&mut self, destination: Option<&str>) { self.destination = destination.map(str::to_string); self.route(); } - pub fn balanced(&self) -> bool { - self.balanced - } + pub fn balanced(&self) -> bool { self.balanced } - pub fn set_balanced(&mut self, balanced: bool) { - self.balanced = balanced; - } + pub fn set_balanced(&mut self, balanced: bool) { self.balanced = balanced; } - pub fn external(&self) -> bool { - self.external - } + pub fn external(&self) -> bool { self.external } - pub fn set_external(&mut self, external: bool) { - self.external = external; - } + pub fn set_external(&mut self, external: bool) { self.external = external; } - pub fn set_leading(&mut self, leading: bool) { - self.leading = leading; - } + pub fn set_leading(&mut self, leading: bool) { self.leading = leading; } - pub fn node(&self) -> Option { - self.node.clone() - } + pub fn node(&self) -> Option { self.node.clone() } pub fn set_node(&mut self, node: &Node) { self.node = Some(node.to_owned()); } - pub fn node_id(&self) -> Option { - self.node_id.clone() - } + pub fn node_id(&self) -> Option { self.node_id.clone() } pub fn set_node_id(&mut self, id: &str) { self.node_id = Some(id.to_owned()); @@ -105,7 +83,8 @@ impl Parseable for Anchor { fn render(&self) -> String { let Some(destination) = &self.destination else { panic!( - "Attempt to render anchor {self:#?} without knowing its destination." + "Attempt to render anchor {self:#?} without knowing \ + its destination." ) }; @@ -131,9 +110,7 @@ impl Parseable for Anchor { ) } - fn flatten(&self) -> String { - self.text.clone() - } + fn flatten(&self) -> String { self.text.clone() } } impl std::fmt::Display for Anchor { @@ -177,9 +154,8 @@ impl std::fmt::Display for Anchor { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render_anchor() { @@ -188,7 +164,10 @@ mod tests { anchor.set_destination(Some("AnchorDest")); assert_eq!( anchor.render(), - r#"AnchorText"# + concat!( + r#"AnchorText"#, + ) ); } @@ -196,9 +175,7 @@ mod tests { #[should_panic( expected = "Attempt to lex an anchor directly from a lexeme" )] - fn lex() { - Anchor::lex(&Lexeme::default()); - } + fn lex() { Anchor::lex(&Lexeme::default()); } #[test] #[should_panic(expected = "without knowing its destination")] diff --git a/src/syntax/content/parser/token/bold.rs b/src/syntax/content/parser/token/bold.rs index 0252a67..558e091 100644 --- a/src/syntax/content/parser/token/bold.rs +++ b/src/syntax/content/parser/token/bold.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Bold { @@ -8,15 +6,11 @@ pub struct Bold { } impl Bold { - pub fn new(open: bool) -> Bold { - Bold { open } - } + pub fn new(open: bool) -> Bold { Bold { open } } } impl Parseable for Bold { - fn probe(lexeme: &Lexeme) -> bool { - lexeme.text() == "*" - } + fn probe(lexeme: &Lexeme) -> bool { lexeme.text() == "*" } fn lex(_lexeme: &Lexeme) -> Bold { panic!("Attempt to lex a bold tag directly from a lexeme") @@ -30,9 +24,7 @@ impl Parseable for Bold { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Bold { @@ -44,9 +36,8 @@ impl std::fmt::Display for Bold { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render() { @@ -61,9 +52,7 @@ mod tests { #[should_panic( expected = "Attempt to lex a bold tag directly from a lexeme" )] - fn lex() { - Bold::lex(&Lexeme::default()); - } + fn lex() { Bold::lex(&Lexeme::default()); } #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/checkbox.rs b/src/syntax/content/parser/token/checkbox.rs index 777e499..b740e2b 100644 --- a/src/syntax/content/parser/token/checkbox.rs +++ b/src/syntax/content/parser/token/checkbox.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct CheckBox { @@ -8,9 +6,7 @@ pub struct CheckBox { } impl CheckBox { - pub fn new(checked: bool) -> CheckBox { - CheckBox { checked } - } + pub fn new(checked: bool) -> CheckBox { CheckBox { checked } } } impl Parseable for CheckBox { @@ -34,9 +30,7 @@ impl Parseable for CheckBox { format!(r#""#) } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for CheckBox { @@ -48,9 +42,8 @@ impl std::fmt::Display for CheckBox { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render() { diff --git a/src/syntax/content/parser/token/code.rs b/src/syntax/content/parser/token/code.rs index 4aef324..35a6fd3 100644 --- a/src/syntax/content/parser/token/code.rs +++ b/src/syntax/content/parser/token/code.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Code { @@ -8,15 +6,11 @@ pub struct Code { } impl Code { - pub fn new(open: bool) -> Code { - Code { open } - } + pub fn new(open: bool) -> Code { Code { open } } } impl Parseable for Code { - fn probe(lexeme: &Lexeme) -> bool { - lexeme.text() == "`" - } + fn probe(lexeme: &Lexeme) -> bool { lexeme.text() == "`" } fn lex(_lexeme: &Lexeme) -> Code { panic!("Attempt to lex a code tag directly from a lexeme") @@ -30,9 +24,7 @@ impl Parseable for Code { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Code { @@ -44,9 +36,8 @@ impl std::fmt::Display for Code { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render() { @@ -61,9 +52,7 @@ mod tests { #[should_panic( expected = "Attempt to lex a code tag directly from a lexeme" )] - fn lex() { - Code::lex(&Lexeme::default()); - } + fn lex() { Code::lex(&Lexeme::default()); } #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/header.rs b/src/syntax/content/parser/token/header.rs index e18603c..88f1643 100644 --- a/src/syntax/content/parser/token/header.rs +++ b/src/syntax/content/parser/token/header.rs @@ -1,15 +1,14 @@ use std::{ collections::{HashMap, hash_map::Entry}, + fmt::Display, }; use crate::{ - prelude::*, graph::Config, - syntax::content::{Parseable, Lexeme}, + prelude::*, + syntax::content::{Lexeme, Parseable}, }; -use std::fmt::Display; - #[derive(Debug, Clone, Eq, PartialEq)] pub struct Header { open: Option, @@ -113,9 +112,7 @@ impl Parseable for Header { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Header { @@ -195,9 +192,8 @@ impl Display for Level { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn make_id() { diff --git a/src/syntax/content/parser/token/item.rs b/src/syntax/content/parser/token/item.rs index 7652898..a0b8321 100644 --- a/src/syntax/content/parser/token/item.rs +++ b/src/syntax/content/parser/token/item.rs @@ -1,4 +1,4 @@ -use crate::syntax::content::{Parseable, Lexeme}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Default, Debug, Clone, Eq, PartialEq)] pub struct Item { @@ -7,9 +7,7 @@ pub struct Item { } impl Parseable for Item { - fn probe(_: &Lexeme) -> bool { - false - } + fn probe(_: &Lexeme) -> bool { false } fn lex(_: &Lexeme) -> Item { panic!("Attempt to lex an item directly from a lexeme") @@ -19,9 +17,7 @@ impl Parseable for Item { panic!("Items should only be rendered by a list's render method") } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl Item { @@ -50,9 +46,8 @@ impl std::fmt::Display for Item { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] #[should_panic( diff --git a/src/syntax/content/parser/token/linebreak.rs b/src/syntax/content/parser/token/linebreak.rs index 6996815..25e3d80 100644 --- a/src/syntax/content/parser/token/linebreak.rs +++ b/src/syntax/content/parser/token/linebreak.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, parser::Lexeme}, -}; +use crate::syntax::content::{Parseable, parser::Lexeme}; #[derive(Default, Debug, Clone, Eq, PartialEq)] pub struct LineBreak {} @@ -10,17 +8,11 @@ impl Parseable for LineBreak { lexeme.match_char('<') && lexeme.match_next_char('\n') } - fn lex(_lexeme: &Lexeme) -> LineBreak { - LineBreak {} - } + fn lex(_lexeme: &Lexeme) -> LineBreak { LineBreak {} } - fn render(&self) -> String { - String::from("
") - } + fn render(&self) -> String { String::from("
") } - fn flatten(&self) -> String { - String::from('\n') - } + fn flatten(&self) -> String { String::from('\n') } } impl std::fmt::Display for LineBreak { @@ -31,9 +23,8 @@ impl std::fmt::Display for LineBreak { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs index 82deb46..fa2817e 100644 --- a/src/syntax/content/parser/token/list.rs +++ b/src/syntax/content/parser/token/list.rs @@ -27,8 +27,8 @@ impl Parseable for List { /// - Strict division is performed but related panics are unreachable given /// the guarantees described in `List::scale_indent` /// - Saturates subtractions from indent levels at zero. This is not - /// unreachable, but a difference of zero is a no-op considering it - /// would cause an iteration of zero times (over an empty range). + /// unreachable, but a difference of zero is a no-op considering it would + /// cause an iteration of zero times (over an empty range). fn render(&self) -> String { let tag = if self.ordered { "ol" } else { "ul" }; let mut output = String::new(); @@ -122,9 +122,8 @@ impl std::fmt::Display for List { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render_flat_list() { diff --git a/src/syntax/content/parser/token/literal.rs b/src/syntax/content/parser/token/literal.rs index ca521ae..6991fb0 100644 --- a/src/syntax/content/parser/token/literal.rs +++ b/src/syntax/content/parser/token/literal.rs @@ -16,13 +16,9 @@ impl Parseable for Literal { } } - fn render(&self) -> String { - self.text.clone() - } + fn render(&self) -> String { self.text.clone() } - fn flatten(&self) -> String { - self.text.clone() - } + fn flatten(&self) -> String { self.text.clone() } } impl std::fmt::Display for Literal { @@ -33,9 +29,8 @@ impl std::fmt::Display for Literal { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/oblique.rs b/src/syntax/content/parser/token/oblique.rs index 0d69468..124e8fa 100644 --- a/src/syntax/content/parser/token/oblique.rs +++ b/src/syntax/content/parser/token/oblique.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Oblique { @@ -8,15 +6,11 @@ pub struct Oblique { } impl Oblique { - pub fn new(open: bool) -> Oblique { - Oblique { open } - } + pub fn new(open: bool) -> Oblique { Oblique { open } } } impl Parseable for Oblique { - fn probe(lexeme: &Lexeme) -> bool { - lexeme.text() == "_" - } + fn probe(lexeme: &Lexeme) -> bool { lexeme.text() == "_" } fn lex(_lexeme: &Lexeme) -> Oblique { panic!("Attempt to lex an oblique tag directly from a lexeme") @@ -30,9 +24,7 @@ impl Parseable for Oblique { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Oblique { @@ -44,9 +36,8 @@ impl std::fmt::Display for Oblique { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render() { @@ -61,9 +52,7 @@ mod tests { #[should_panic( expected = "Attempt to lex an oblique tag directly from a lexeme" )] - fn lex() { - Oblique::lex(&Lexeme::default()); - } + fn lex() { Oblique::lex(&Lexeme::default()); } #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/paragraph.rs b/src/syntax/content/parser/token/paragraph.rs index 1c51d87..963bfe3 100644 --- a/src/syntax/content/parser/token/paragraph.rs +++ b/src/syntax/content/parser/token/paragraph.rs @@ -6,9 +6,7 @@ pub struct Paragraph { } impl Paragraph { - pub fn new(open: bool) -> Paragraph { - Paragraph { open: Some(open) } - } + pub fn new(open: bool) -> Paragraph { Paragraph { open: Some(open) } } pub fn probe_end(lexeme: &Lexeme) -> bool { lexeme.match_char('\n') && lexeme.match_next_char('\n') @@ -21,9 +19,7 @@ impl Parseable for Paragraph { !lexeme.is_whitespace() } - fn lex(_lexeme: &Lexeme) -> Paragraph { - Paragraph { open: None } - } + fn lex(_lexeme: &Lexeme) -> Paragraph { Paragraph { open: None } } fn render(&self) -> String { if let Some(open) = self.open { @@ -39,9 +35,7 @@ impl Parseable for Paragraph { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Paragraph { @@ -62,9 +56,8 @@ impl std::fmt::Display for Paragraph { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn lex() { @@ -73,9 +66,8 @@ mod tests { } #[test] - #[should_panic( - expected = "Attempt to render a paragraph tag while open state is unknown" - )] + #[should_panic(expected = "Attempt to render a paragraph tag while \ + open state is unknown")] fn render_state_unknown() { let p = Paragraph::lex(&Lexeme::default()); drop(p.render()); diff --git a/src/syntax/content/parser/token/preformat.rs b/src/syntax/content/parser/token/preformat.rs index 89e71dc..fbddf8a 100644 --- a/src/syntax/content/parser/token/preformat.rs +++ b/src/syntax/content/parser/token/preformat.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct PreFormat { @@ -8,9 +6,7 @@ pub struct PreFormat { } impl PreFormat { - pub fn new(open: bool) -> PreFormat { - PreFormat { open: Some(open) } - } + pub fn new(open: bool) -> PreFormat { PreFormat { open: Some(open) } } } impl std::fmt::Display for PreFormat { @@ -29,9 +25,7 @@ impl Parseable for PreFormat { lexeme.match_first_char('`') && (lexeme.next() == "\n" || lexeme.last()) } - fn lex(_lexeme: &Lexeme) -> PreFormat { - PreFormat { open: None } - } + fn lex(_lexeme: &Lexeme) -> PreFormat { PreFormat { open: None } } fn render(&self) -> String { if let Some(o) = self.open { @@ -47,16 +41,13 @@ impl Parseable for PreFormat { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn lex() { @@ -68,9 +59,8 @@ mod tests { } #[test] - #[should_panic( - expected = "Attempt to render a preformat tag while open state is unknown" - )] + #[should_panic(expected = "Attempt to render a preformat tag while \ + open state is unknown")] fn render() { let from_empty_lexeme = PreFormat::lex(&Lexeme::default()); from_empty_lexeme.render(); diff --git a/src/syntax/content/parser/token/quote.rs b/src/syntax/content/parser/token/quote.rs index ab0bd36..a6d144d 100644 --- a/src/syntax/content/parser/token/quote.rs +++ b/src/syntax/content/parser/token/quote.rs @@ -26,9 +26,7 @@ impl Parseable for Quote { lexeme.match_char('>') && lexeme.match_next_char(' ') } - fn lex(_lexeme: &Lexeme) -> Quote { - Quote::default() - } + fn lex(_lexeme: &Lexeme) -> Quote { Quote::default() } fn render(&self) -> String { let opening = if let Some(url) = &self.url { @@ -49,9 +47,7 @@ impl Parseable for Quote { format!("\n{opening}\n{content}\n
\n") } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Quote { diff --git a/src/syntax/content/parser/token/strike.rs b/src/syntax/content/parser/token/strike.rs index 14b4407..ae2f9e3 100644 --- a/src/syntax/content/parser/token/strike.rs +++ b/src/syntax/content/parser/token/strike.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Strike { @@ -8,9 +6,7 @@ pub struct Strike { } impl Strike { - pub fn new(open: bool) -> Strike { - Strike { open } - } + pub fn new(open: bool) -> Strike { Strike { open } } } impl Parseable for Strike { @@ -27,9 +23,7 @@ impl Parseable for Strike { String::from(tag) } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Strike { @@ -41,9 +35,8 @@ impl std::fmt::Display for Strike { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render() { @@ -58,9 +51,7 @@ mod tests { #[should_panic( expected = "Attempt to lex a strike tag directly from a lexeme" )] - fn lex() { - Strike::lex(&Lexeme::default()); - } + fn lex() { Strike::lex(&Lexeme::default()); } #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/table.rs b/src/syntax/content/parser/token/table.rs index cf5cb51..2eb29e3 100644 --- a/src/syntax/content/parser/token/table.rs +++ b/src/syntax/content/parser/token/table.rs @@ -15,9 +15,7 @@ impl Table { self.headers.push(header.trim().to_string()); } - pub fn add_row(&mut self, row: Vec) { - self.contents.push(row); - } + pub fn add_row(&mut self, row: Vec) { self.contents.push(row); } pub fn add_cell(&mut self, content: &str) { if let Some(last) = self.contents.last_mut() { @@ -37,13 +35,9 @@ impl Table { } impl Parseable for Table { - fn probe(lexeme: &Lexeme) -> bool { - lexeme.match_char_sequence('%', '\n') - } + fn probe(lexeme: &Lexeme) -> bool { lexeme.match_char_sequence('%', '\n') } - fn lex(_lexeme: &Lexeme) -> Table { - Table::default() - } + fn lex(_lexeme: &Lexeme) -> Table { Table::default() } fn render(&self) -> String { let mut xml = String::from("\n\n"); @@ -73,9 +67,7 @@ impl Parseable for Table { xml } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Table { diff --git a/src/syntax/content/parser/token/underline.rs b/src/syntax/content/parser/token/underline.rs index 2da61b7..1fbbdda 100644 --- a/src/syntax/content/parser/token/underline.rs +++ b/src/syntax/content/parser/token/underline.rs @@ -1,6 +1,4 @@ -use crate::{ - syntax::content::{Parseable, Lexeme}, -}; +use crate::syntax::content::{Lexeme, Parseable}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Underline { @@ -8,9 +6,7 @@ pub struct Underline { } impl Underline { - pub fn new(open: bool) -> Underline { - Underline { open } - } + pub fn new(open: bool) -> Underline { Underline { open } } } impl Parseable for Underline { @@ -30,9 +26,7 @@ impl Parseable for Underline { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Underline { @@ -44,9 +38,8 @@ impl std::fmt::Display for Underline { #[cfg(test)] mod tests { - use crate::syntax::content::parser::Token; - use super::*; + use crate::syntax::content::parser::Token; #[test] fn render() { @@ -61,9 +54,7 @@ mod tests { #[should_panic( expected = "Attempt to lex an underline tag directly from a lexeme" )] - fn lex() { - Underline::lex(&Lexeme::default()); - } + fn lex() { Underline::lex(&Lexeme::default()); } #[test] fn token_display() { diff --git a/src/syntax/content/parser/token/verse.rs b/src/syntax/content/parser/token/verse.rs index 6dbeb9b..b690b57 100644 --- a/src/syntax/content/parser/token/verse.rs +++ b/src/syntax/content/parser/token/verse.rs @@ -43,9 +43,7 @@ impl Parseable for Verse { } } - fn flatten(&self) -> String { - String::default() - } + fn flatten(&self) -> String { String::default() } } impl std::fmt::Display for Verse { From 2e95c94f54837273bee230271fb256f316ecf932 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 16:17:57 -0300 Subject: [PATCH 033/108] Change tests running order --- .justfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.justfile b/.justfile index 0b23ad4..0166cf6 100644 --- a/.justfile +++ b/.justfile @@ -47,8 +47,8 @@ alias qr := quick-assess-run-watch [private] quick-test-cover: - {{ cover_cmd }} --no-report -- --skip 'serial_tests::' {{ cover_cmd }} --no-report -- --test 'serial_tests::' --test-threads 1 + {{ cover_cmd }} --no-report -- --skip 'serial_tests::' {{ cover_cmd }} report --html @{{ cover_cmd }} report | tail -1 | awk '{ print " [ Regions:", $4, "• Functions:", $7, "• Lines:", $10, "]" }' @@ -185,8 +185,10 @@ alias c := check # Run tests [group: 'assess'] test: - cargo test -- --skip 'serial_tests::' cargo test -- --test 'serial_tests::' --test-threads 1 + cargo test --bin en + cargo test --doc + cargo test --lib -- --skip 'serial_tests::' alias t := test @@ -200,8 +202,8 @@ alias oc := test-cover-clean # Run tests with coverage [group: 'assess'] test-cover: test-cover-clean - {{ cover_cmd }} --no-report -- --skip 'serial_tests::' {{ cover_cmd }} --no-report -- --test 'serial_tests::' --test-threads 1 + {{ cover_cmd }} --no-report -- --skip 'serial_tests::' alias o := test-cover From 5b5541c28f5b7237019772b37d9962d2f55ad942 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 16 Feb 2026 23:31:18 -0300 Subject: [PATCH 034/108] Adopt and apply nightly clippy configuration --- .justfile | 16 +- Cargo.toml | 200 ++++++++++++------- src/graph.rs | 21 +- src/graph/meta.rs | 8 +- src/lib.rs | 2 +- src/log.rs | 23 ++- src/main.rs | 2 +- src/router/handlers/graph.rs | 11 +- src/router/handlers/navigation.rs | 2 +- src/router/handlers/template.rs | 4 +- src/syntax/command.rs | 2 +- src/syntax/content/parser.rs | 2 +- src/syntax/content/parser/context/anchor.rs | 4 +- src/syntax/content/parser/context/block.rs | 2 +- src/syntax/content/parser/context/inline.rs | 3 +- src/syntax/content/parser/context/list.rs | 2 +- src/syntax/content/parser/context/quote.rs | 2 +- src/syntax/content/parser/context/table.rs | 2 +- src/syntax/content/parser/lexeme.rs | 6 +- src/syntax/content/parser/point.rs | 6 +- src/syntax/content/parser/token/anchor.rs | 18 +- src/syntax/content/parser/token/bold.rs | 2 +- src/syntax/content/parser/token/checkbox.rs | 2 +- src/syntax/content/parser/token/code.rs | 2 +- src/syntax/content/parser/token/header.rs | 6 +- src/syntax/content/parser/token/linebreak.rs | 2 +- src/syntax/content/parser/token/list.rs | 10 +- src/syntax/content/parser/token/oblique.rs | 2 +- src/syntax/content/parser/token/paragraph.rs | 2 +- src/syntax/content/parser/token/preformat.rs | 2 +- src/syntax/content/parser/token/strike.rs | 2 +- src/syntax/content/parser/token/underline.rs | 2 +- src/syntax/content/parser/token/verse.rs | 2 +- 33 files changed, 234 insertions(+), 140 deletions(-) diff --git a/.justfile b/.justfile index 0166cf6..9990d42 100644 --- a/.justfile +++ b/.justfile @@ -69,7 +69,7 @@ alias f := format # Lint [group: 'develop'] lint: - cargo clippy + cargo +nightly clippy alias l := lint @@ -94,9 +94,16 @@ rustc-fix: alias rf := rustc-fix +# Apply clippy lint fixes +[group: 'develop'] +clippy-fix: + cargo +nightly clippy --fix --allow-dirty + +alias cf := clippy-fix + # Apply all automatic fixes [group: 'develop'] -fix: rustc-fix format +fix: rustc-fix clippy-fix format alias x := fix @@ -169,8 +176,9 @@ alias fc := format-assess # Assess production lints [group: 'assess'] lint-assess: - cargo clippy -- \ - -D clippy::dbg_macro -D clippy::print_stdout -D clippy::print_stderr \ + cargo +nightly clippy -- \ + -D clippy::dbg_macro -D clippy::use_debug \ + -D clippy::print_stdout -D clippy::print_stderr \ -D clippy::todo -D clippy::unimplemented -D clippy::unreachable alias la := lint-assess diff --git a/Cargo.toml b/Cargo.toml index f0e85d5..fe2cf4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,101 +34,205 @@ let_underscore= { level = "warn", priority = 10 } nonstandard-style = "warn" future-incompatible = "warn" keyword-idents = "warn" +non_ascii_idents = "warn" [lints.clippy] # levels: allow, warn, deny, forbid -manual_non_exhaustive = "allow" -collapsible_if = "allow" -collapsible_else_if = "allow" -field_reassign_with_default = "allow" - -# pedantic +allow_attributes = "warn" +arithmetic_side_effects = "warn" +as_conversions = "warn" +as_pointer_underscore = "warn" +as_underscore = "warn" assigning_clones = "warn" -borrow_as_ptr = "warn" +branches_sharing_code = "warn" +case_sensitive_file_extension_comparisons = "warn" cast_lossless = "warn" +cast_possible_truncation = "warn" cast_possible_wrap = "warn" -cast_ptr_alignment = "warn" +cast_precision_loss = "warn" cast_sign_loss = "warn" checked_conversions = "warn" +clear_with_drain = "warn" cloned_instead_of_copied = "warn" +coerce_container_to_any = "warn" +collapsible_else_if = "allow" +collapsible_if = "allow" +collection_is_never_read = "warn" +comparison_chain = "warn" copy_iterator = "warn" default_trait_access = "warn" +deref_by_slicing = "warn" +derive_partial_eq_without_eq = "warn" doc_broken_link = "warn" doc_comment_double_space_linebreaks = "warn" +doc_include_without_cfg = "warn" +doc_link_code = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" +doc_paragraphs_missing_punctuation = "warn" +duration_suboptimal_units = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" empty_enums = "warn" +empty_structs_with_brackets = "warn" +equatable_if_let = "warn" +error_impl_error = "warn" +exit = "warn" +expect_used = "warn" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" explicit_iter_loop = "warn" +fallible_impl_from = "warn" +field_reassign_with_default = "allow" +filetype_is_file = "warn" filter_map_next = "warn" flat_map_option = "warn" float_cmp = "warn" +float_cmp_const = "warn" +fn_to_numeric_cast_any = "warn" +format_collect = "warn" +format_push_string = "warn" from_iter_instead_of_collect = "warn" +get_unwrap = "warn" if_not_else = "warn" +if_then_some_else_none = "warn" ignore_without_reason = "warn" ignored_unit_patterns = "warn" implicit_clone = "warn" implicit_hasher = "warn" +imprecise_flops = "warn" inconsistent_struct_constructor = "warn" index_refutable_slice = "warn" +indexing_slicing = "warn" inefficient_to_string = "warn" +infinite_loop = "warn" +integer_division = "warn" +integer_division_remainder_used = "warn" +into_iter_without_iter = "warn" invalid_upcast_comparisons = "warn" ip_constant = "warn" iter_filter_is_ok = "warn" iter_filter_is_some = "warn" +iter_not_returning_iterator = "warn" +iter_on_empty_collections = "warn" +iter_on_single_items = "warn" +iter_with_drain = "warn" +iter_without_into_iter = "warn" large_digit_groups = "warn" large_futures = "warn" large_stack_arrays = "warn" large_types_passed_by_value = "warn" +let_underscore_must_use = "warn" linkedlist = "warn" +literal_string_with_formatting_args = "warn" +lossy_float_literal = "warn" +macro_use_imports = "warn" manual_assert = "warn" +manual_ilog2 = "warn" manual_instant_elapsed = "warn" manual_is_power_of_two = "warn" manual_is_variant_and = "warn" manual_let_else = "warn" manual_midpoint = "warn" +manual_non_exhaustive = "allow" manual_string_new = "warn" many_single_char_names = "warn" -map_unwrap_or = "warn" +map_err_ignore = "warn" +map_with_unused_argument_over_ranges = "warn" match_bool = "warn" match_same_arms = "warn" match_wild_err_arm = "warn" match_wildcard_for_single_variants = "warn" maybe_infinite_iter = "warn" +mem_forget = "warn" mismatching_type_param_order = "warn" +missing_assert_message = "warn" +missing_asserts_for_indexing = "warn" +missing_const_for_fn = "warn" missing_errors_doc = "warn" missing_fields_in_debug = "warn" missing_panics_doc = "warn" +mixed_read_write_in_expression = "warn" +mod_module_files = "warn" +module_name_repetitions = "warn" +multiple_inherent_impl = "warn" mut_mut = "warn" +mutex_atomic = "warn" +mutex_integer = "warn" naive_bytecount = "warn" +needless_collect = "warn" needless_continue = "warn" needless_for_each = "warn" +needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" needless_raw_string_hashes = "warn" +needless_raw_strings = "warn" +needless_type_cast = "warn" no_effect_underscore_binding = "warn" +non_send_fields_in_send_ty = "warn" +non_std_lazy_statics = "warn" +non_zero_suggestions = "warn" +nonstandard_macro_braces = "warn" option_as_ref_cloned = "warn" option_option = "warn" -ptr_as_ptr = "warn" -ptr_cast_constness = "warn" +panic_in_result_fn = "warn" +path_buf_push_overwrite = "warn" +pathbuf_init_then_push = "warn" pub_underscore_fields = "warn" +pub_without_shorthand = "warn" range_minus_one = "warn" range_plus_one = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +read_zero_byte_vec = "warn" +redundant_clone = "warn" redundant_closure_for_method_calls = "warn" -ref_as_ptr = "warn" +redundant_pub_crate = "warn" +redundant_test_prefix = "warn" +redundant_type_annotations = "warn" ref_binding_to_reference = "warn" ref_option = "warn" ref_option_ref = "warn" +renamed_function_params = "warn" +rest_pat_in_fully_bound_structs = "warn" +return_and_then = "warn" return_self_not_must_use = "warn" same_functions_in_if_condition = "warn" +same_length_and_capacity = "warn" +same_name_method = "warn" +search_is_some = "warn" +self_only_used_in_recursion = "warn" semicolon_if_nothing_returned = "warn" +semicolon_inside_block = "warn" set_contains_or_insert = "warn" +shadow_reuse = "warn" +shadow_same = "warn" +shadow_unrelated = "warn" should_panic_without_expect = "warn" +similar_names = "warn" +single_char_pattern = "warn" +single_match_else = "warn" +single_option_map = "warn" +stable_sort_primitive = "warn" str_split_at_newline = "warn" +string_add = "warn" +string_add_assign = "warn" +string_lit_as_bytes = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" struct_field_names = "warn" +suboptimal_flops = "warn" +suspicious_operation_groupings = "warn" +suspicious_xor_used_as_pow = "warn" +tests_outside_test_module = "warn" +too_long_first_doc_paragraph = "warn" +trait_duplication_in_bounds = "warn" trivially_copy_pass_by_ref = "warn" +try_err = "warn" +tuple_array_conversions = "warn" +type_repetition_in_bounds = "warn" unchecked_time_subtraction = "warn" unicode_not_nfc = "warn" uninlined_format_args = "warn" @@ -136,72 +240,34 @@ unnecessary_box_returns = "warn" unnecessary_debug_formatting = "warn" unnecessary_join = "warn" unnecessary_literal_bound = "warn" +unnecessary_self_imports = "warn" unnecessary_semicolon = "warn" +unnecessary_struct_initialization = "warn" unnecessary_wraps = "warn" +unneeded_field_pattern = "warn" unnested_or_patterns = "warn" unreadable_literal = "warn" unsafe_derive_deserialize = "warn" +unseparated_literal_suffix = "warn" unused_async = "warn" +unused_peekable = "warn" +unused_result_ok = "warn" +unused_rounding = "warn" unused_self = "warn" +unused_trait_names = "warn" +unwrap_in_result = "warn" +unwrap_used = "warn" used_underscore_binding = "warn" +used_underscore_items = "warn" +useless_let_if_seq = "warn" +verbose_file_reads = "warn" +volatile_composites = "warn" +wildcard_enum_match_arm = "warn" wildcard_imports = "warn" zero_sized_map_values = "warn" -# restrictive -arithmetic_side_effects = "warn" -as_conversions = "warn" -as_pointer_underscore = "warn" -as_underscore = "warn" -deref_by_slicing = "warn" -empty_drop = "warn" -empty_enum_variants_with_brackets = "warn" -error_impl_error = "warn" -exit = "warn" -expect_used = "warn" -filetype_is_file = "warn" -float_cmp_const = "warn" -fn_to_numeric_cast_any = "warn" -if_then_some_else_none = "warn" -indexing_slicing = "warn" -infinite_loop = "warn" -integer_division = "warn" -integer_division_remainder_used = "warn" -let_underscore_must_use = "warn" -let_underscore_untyped = "warn" -lossy_float_literal = "warn" -map_err_ignore = "warn" -map_with_unused_argument_over_ranges = "warn" -missing_assert_message = "warn" -missing_asserts_for_indexing = "warn" -mixed_read_write_in_expression = "warn" -module_name_repetitions = "warn" -multiple_inherent_impl = "warn" -needless_raw_strings = "warn" -non_zero_suggestions = "warn" -panic_in_result_fn = "warn" -pathbuf_init_then_push = "warn" -pub_without_shorthand = "warn" -redundant_test_prefix = "warn" -redundant_type_annotations = "warn" -renamed_function_params = "warn" -rest_pat_in_fully_bound_structs = "warn" -return_and_then = "warn" -same_name_method = "warn" -semicolon_outside_block = "warn" -shadow_reuse = "warn" -shadow_same = "warn" -shadow_unrelated = "warn" -string_add = "warn" -string_lit_chars_any = "warn" -unnecessary_self_imports = "warn" -unneeded_field_pattern = "warn" -unseparated_literal_suffix = "warn" -unused_result_ok = "warn" -unused_trait_names = "warn" -unwrap_used = "warn" -verbose_file_reads = "warn" -wildcard_enum_match_arm = "warn" - # cargo negative_feature_names = "warn" redundant_feature_names = "warn" +multiple_crate_versions = "warn" +wildcard_dependencies = "warn" diff --git a/src/graph.rs b/src/graph.rs index ca64d65..39ee664 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -78,7 +78,7 @@ impl Graph { } } - /// Takes a file path to a TOML file and returns a modulated Graph + /// Takes a file path to a TOML file and returns a modulated Graph. /// /// If `path` is None, it will fallback to CLI arguments or their defaults. /// @@ -99,11 +99,18 @@ impl Graph { let cli_path = Arguments::default().parse().graph_path; let path = in_path.map_or(cli_path, PathBuf::from); - let toml_source = match std::fs::read_to_string(path) { + let toml_source = match std::fs::read_to_string(&path) { Ok(s) => s, Err(e) => { - log!(ERROR, "Failed reading {e}"); - return Err("Failed reading file at {path}".to_string()); + log!( + ERROR, + "Error reading path {}: {e}", + path.as_path().display(), + ); + return Err(format!( + "Failed reading file at {}", + path.as_path().display(), + )); }, }; @@ -192,7 +199,7 @@ impl Graph { tlog!(&instant, "Parsed configuration"); } - /// Construct a `HashMap` with incoming connections (reversed edges) + /// Construct a `HashMap` with incoming connections (reversed edges). fn map_incoming(&mut self) { for node in self.nodes.clone().into_values() { for edge in node.connections.clone().values() { @@ -372,7 +379,7 @@ impl Graph { } } - /// Increments detached node statistics for the given node ID + /// Increments detached node statistics for the given node ID. /// /// Performs checked arithmetic to the following effect: /// - Stats will saturate at `u32::MAX` @@ -392,7 +399,7 @@ impl Graph { } pub fn find_node(&self, query: &str) -> QueryResult { - let collapsed_query = query.trim().replace(" ", ""); + let collapsed_query = query.trim().replace(' ', ""); if query == collapsed_query { log!(VERBOSE, "Chasing candidate for query {query}"); diff --git a/src/graph/meta.rs b/src/graph/meta.rs index f3669fb..d94406a 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -105,9 +105,9 @@ impl Default for Config { } // See: https://github.com/serde-rs/serde/issues/368 -fn mktrue() -> bool { true } -fn mkfalse() -> bool { false } -fn mk8() -> u16 { 8 } +const fn mktrue() -> bool { true } +const fn mkfalse() -> bool { false } +const fn mk8() -> u16 { 8 } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct Version { @@ -148,7 +148,7 @@ impl Version { Version::from_text(env!("CARGO_PKG_VERSION")) } - /// Parses a string into a Version struct + /// Parses a string into a Version struct. /// /// It is expected for the version string to contain exactly three /// dot-separated numeric values with an optional leading `v` character. diff --git a/src/lib.rs b/src/lib.rs index 2cbd04c..473bb4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ pub mod prelude { pub use crate::{ log, log::{Level::*, now}, - tlog, + tlog, write_log, }; } diff --git a/src/log.rs b/src/log.rs index a079d1e..ebc6eb7 100644 --- a/src/log.rs +++ b/src/log.rs @@ -4,7 +4,7 @@ pub use level::*; mod level; -/// Strings in this slice suppress logging if found in the stack trace +/// Strings in this slice suppress logging if found in the stack trace. pub const EXCLUSIONS: &[&str] = &["en::graph::Graph::parse_config"]; #[derive(Debug)] @@ -46,7 +46,7 @@ impl Data { && !excluded_by_env && matches_filter; - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] if env_level == Level::META { eprintln!( "Log decision for message from {path}: {should_log} given\n\ @@ -79,7 +79,7 @@ pub fn env_level() -> Level { } } -#[allow(clippy::print_stderr)] +#[expect(clippy::print_stderr)] pub fn print_state() { let env_level = env_level(); let version = env!("CARGO_PKG_VERSION"); @@ -90,7 +90,7 @@ pub fn print_state() { } } -#[allow(clippy::print_stderr)] +#[expect(clippy::print_stderr)] pub fn timed(past: &Instant, message: &str) -> Instant { let now = Instant::now(); let env_level = env_level(); @@ -120,7 +120,7 @@ macro_rules! tlog { pub fn now() -> Instant { Instant::now() } -#[allow(clippy::print_stderr)] +#[expect(clippy::print_stderr, clippy::use_debug)] pub fn elog(function: &str, message: &str) { eprintln!("{:?} [{function}] {message}", crate::ONSET.elapsed()); } @@ -214,6 +214,17 @@ pub fn wrap(s: &str) -> String { symbolize("e(&escape(s))) } +#[macro_export] +macro_rules! write_log { + ($buffer:expr, $format_string:expr $(, $format_args:expr)* $(,)?) => {{ + use std::fmt::Write as _; + let result = write!($buffer, $format_string $(, $format_args)*); + if let Err(error) = result { + log!(ERROR, "Unexpected error writing into {}: ${error}", $buffer); + } + }}; +} + #[cfg(test)] mod tests { use super::*; @@ -239,7 +250,7 @@ mod tests { } fn run_in_debug_level(level: &str) { - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { std::env::set_var("DEBUG", level); log!("Debug is set to {level}"); diff --git a/src/main.rs b/src/main.rs index b23dc2d..f561e2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{backtrace, io, panic}; use en::{ONSET, graph::Graph, log, prelude::*, syntax}; #[tokio::main] -#[allow(clippy::print_stderr, clippy::print_stdout)] +#[expect(clippy::print_stderr, clippy::print_stdout, clippy::use_debug)] async fn main() -> io::Result<()> { log::print_state(); let mut instant = now(); diff --git a/src/router/handlers/graph.rs b/src/router/handlers/graph.rs index 349efb7..bf0b112 100644 --- a/src/router/handlers/graph.rs +++ b/src/router/handlers/graph.rs @@ -37,13 +37,10 @@ pub async fn node( "node", &context, if found { 500 } else { 404 }, - Some( - format!( - "Failed to generate page for node {} (ID {}).", - node.title, id - ) - .to_owned(), - ), + Some(format!( + "Failed to generate page for node {} (ID {}).", + node.title, id + )), !found, ) } diff --git a/src/router/handlers/navigation.rs b/src/router/handlers/navigation.rs index f799416..6215edc 100644 --- a/src/router/handlers/navigation.rs +++ b/src/router/handlers/navigation.rs @@ -33,7 +33,7 @@ pub async fn data(State(state): State) -> Response { let mut detached_pairs: Vec<(String, u32)> = state.graph.stats.detached.clone().into_iter().collect(); - detached_pairs.sort_by(|a, b| b.1.cmp(&a.1)); + detached_pairs.sort_by_key(|b| std::cmp::Reverse(b.1)); let mut context = tera::Context::default(); context.insert("graph", &state.graph); diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index a2290d5..1a5f520 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -8,7 +8,7 @@ use crate::{ router::{GlobalState, handlers::raw::make_response}, }; -/// Assembles a response containing the graph as its only context +/// Assembles a response containing the graph as its only context. /// /// The template name **must not** contain the extension. #[expect(clippy::unused_async)] @@ -38,7 +38,7 @@ pub(in crate::router::handlers) fn with_context( make_response(&body, status_code, &[(header::CONTENT_TYPE, "text/html")]) } -/// Renderes a template into a String and error code +/// Renderes a template into a String and error code. /// /// The template name **must not** contain the extension (e.g. `.html`). pub(in crate::router::handlers) fn render( diff --git a/src/syntax/command.rs b/src/syntax/command.rs index 948866f..4d5dd02 100644 --- a/src/syntax/command.rs +++ b/src/syntax/command.rs @@ -7,7 +7,7 @@ use crate::prelude::*; static FIRST_PARSE: AtomicBool = AtomicBool::new(true); -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Arguments { pub hostname: String, pub port: u16, diff --git a/src/syntax/content/parser.rs b/src/syntax/content/parser.rs index 2640edf..42b2680 100644 --- a/src/syntax/content/parser.rs +++ b/src/syntax/content/parser.rs @@ -33,7 +33,7 @@ pub(super) fn rich_read(input: &str, graph: &Graph) -> TokenOutput { } /// Apply end-to-end point and inline parsing for nested formatting, such as -/// inside the display text of anchors and list items +/// inside the display text of anchors and list items. pub fn format(input: &str, graph: &Graph) -> (String, Vec) { let tokens = lex(input, LEXMAP, graph, false).tokens; (parse(&tokens), tokens) diff --git a/src/syntax/content/parser/context/anchor.rs b/src/syntax/content/parser/context/anchor.rs index 1407385..0fbebb6 100644 --- a/src/syntax/content/parser/context/anchor.rs +++ b/src/syntax/content/parser/context/anchor.rs @@ -61,7 +61,7 @@ pub fn parse( && !lexeme.match_next_char('|') { log!(VERBOSE, "End: Plural anchor"); - candidate.set_destination(Some(&candidate.text().clone())); + candidate.set_destination(Some(&candidate.text())); candidate.text_push("s"); if lexeme.last() { push(None, tokens, state, graph); @@ -73,7 +73,7 @@ pub fn parse( if candidate.text().contains(':') { candidate.set_external(true); } - push(Some(&candidate.text().clone()), tokens, state, graph); + push(Some(&candidate.text()), tokens, state, graph); } else { push(Some(&buffer.destination.clone()), tokens, state, graph); } diff --git a/src/syntax/content/parser/context/block.rs b/src/syntax/content/parser/context/block.rs index 63a3ac9..3b34724 100644 --- a/src/syntax/content/parser/context/block.rs +++ b/src/syntax/content/parser/context/block.rs @@ -112,7 +112,7 @@ pub fn parse( iterator.next(); return true; } else if lexeme.match_char('\n') { - tokens.push(Token::LineBreak(LineBreak::default())); + tokens.push(Token::LineBreak(LineBreak)); return true; } }, diff --git a/src/syntax/content/parser/context/inline.rs b/src/syntax/content/parser/context/inline.rs index b4412cc..1f2d208 100644 --- a/src/syntax/content/parser/context/inline.rs +++ b/src/syntax/content/parser/context/inline.rs @@ -48,11 +48,10 @@ pub fn parse( log!(VERBOSE, "Inline Context: Code -> None on {lexeme}"); state.context.inline = Inline::None; tokens.push(Token::Code(Code::new(false))); - return true; } else { tokens.push(Token::Literal(Literal::lex(lexeme))); - return true; } + return true; }, Inline::Anchor => { if context::anchor::parse(lexeme, state, tokens, graph) { diff --git a/src/syntax/content/parser/context/list.rs b/src/syntax/content/parser/context/list.rs index 7c72103..ec53148 100644 --- a/src/syntax/content/parser/context/list.rs +++ b/src/syntax/content/parser/context/list.rs @@ -30,7 +30,7 @@ pub fn parse( let candidate = &mut buffer.candidate; let item_candidate = &mut buffer.item_candidate; - #[allow(clippy::wildcard_enum_match_arm)] + #[expect(clippy::wildcard_enum_match_arm)] match state.context.block { Block::List => { if lexeme.match_char(' ') && item_candidate.depth.is_none() { diff --git a/src/syntax/content/parser/context/quote.rs b/src/syntax/content/parser/context/quote.rs index 08664c1..8b46c22 100644 --- a/src/syntax/content/parser/context/quote.rs +++ b/src/syntax/content/parser/context/quote.rs @@ -26,7 +26,7 @@ pub fn parse( let buffer = &mut state.buffers.quote; let candidate = &mut buffer.candidate; - #[allow(clippy::wildcard_enum_match_arm)] + #[expect(clippy::wildcard_enum_match_arm)] match state.context.block { Block::Quote => { if Quote::probe_end(lexeme) { diff --git a/src/syntax/content/parser/context/table.rs b/src/syntax/content/parser/context/table.rs index a64d136..40ca217 100644 --- a/src/syntax/content/parser/context/table.rs +++ b/src/syntax/content/parser/context/table.rs @@ -32,7 +32,7 @@ pub fn parse( parsed_text }; - #[allow(clippy::wildcard_enum_match_arm)] + #[expect(clippy::wildcard_enum_match_arm)] match state.context.block { Block::Table => { if Table::probe_end(lexeme) { diff --git a/src/syntax/content/parser/lexeme.rs b/src/syntax/content/parser/lexeme.rs index b361fb1..378a8b8 100644 --- a/src/syntax/content/parser/lexeme.rs +++ b/src/syntax/content/parser/lexeme.rs @@ -26,9 +26,9 @@ impl Lexeme { pub fn next(&self) -> String { self.next.clone() } - pub fn last(&self) -> bool { self.last } + pub const fn last(&self) -> bool { self.last } - pub fn first(&self) -> bool { self.first } + pub const fn first(&self) -> bool { self.first } pub fn mutate_text(&mut self, new: &str) { self.text = new.to_string(); } @@ -146,7 +146,7 @@ impl Lexeme { } /// # Panics - /// Panics if number of chars for a single lexeme exceeds `i32::MAX` + /// Panics if number of chars for a single lexeme exceeds `i32::MAX`. pub fn count_char(&self, c: char) -> i32 { let count = self.text().chars().filter(|&n| n == c).count(); match i32::try_from(count) { diff --git a/src/syntax/content/parser/point.rs b/src/syntax/content/parser/point.rs index b97b2e1..6c0b832 100644 --- a/src/syntax/content/parser/point.rs +++ b/src/syntax/content/parser/point.rs @@ -17,9 +17,9 @@ pub fn parse( tokens: &mut Vec, iterator: &mut Peekable>, ) -> bool { - if let super::context::Block::PreFormat = state.context.block { - return false; - } else if let super::context::Inline::Code = state.context.inline { + if matches!(state.context.block, super::context::Block::PreFormat) + || matches!(state.context.inline, super::context::Inline::Code) + { return false; } diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs index 644dc2e..3702d77 100644 --- a/src/syntax/content/parser/token/anchor.rs +++ b/src/syntax/content/parser/token/anchor.rs @@ -28,15 +28,21 @@ impl Anchor { self.route(); } - pub fn balanced(&self) -> bool { self.balanced } + pub const fn balanced(&self) -> bool { self.balanced } - pub fn set_balanced(&mut self, balanced: bool) { self.balanced = balanced; } + pub const fn set_balanced(&mut self, balanced: bool) { + self.balanced = balanced; + } - pub fn external(&self) -> bool { self.external } + pub const fn external(&self) -> bool { self.external } - pub fn set_external(&mut self, external: bool) { self.external = external; } + pub const fn set_external(&mut self, external: bool) { + self.external = external; + } - pub fn set_leading(&mut self, leading: bool) { self.leading = leading; } + pub const fn set_leading(&mut self, leading: bool) { + self.leading = leading; + } pub fn node(&self) -> Option { self.node.clone() } @@ -52,7 +58,7 @@ impl Anchor { fn route(&mut self) { self.destination = if let Some(destination) = self.destination.clone() { - if destination.contains(":") || destination.contains("/") { + if destination.contains(':') || destination.contains('/') { Some(destination) } else if destination.is_empty() && self.text.is_empty() { None diff --git a/src/syntax/content/parser/token/bold.rs b/src/syntax/content/parser/token/bold.rs index 558e091..6ef5bd3 100644 --- a/src/syntax/content/parser/token/bold.rs +++ b/src/syntax/content/parser/token/bold.rs @@ -6,7 +6,7 @@ pub struct Bold { } impl Bold { - pub fn new(open: bool) -> Bold { Bold { open } } + pub const fn new(open: bool) -> Bold { Bold { open } } } impl Parseable for Bold { diff --git a/src/syntax/content/parser/token/checkbox.rs b/src/syntax/content/parser/token/checkbox.rs index b740e2b..c2597b7 100644 --- a/src/syntax/content/parser/token/checkbox.rs +++ b/src/syntax/content/parser/token/checkbox.rs @@ -6,7 +6,7 @@ pub struct CheckBox { } impl CheckBox { - pub fn new(checked: bool) -> CheckBox { CheckBox { checked } } + pub const fn new(checked: bool) -> CheckBox { CheckBox { checked } } } impl Parseable for CheckBox { diff --git a/src/syntax/content/parser/token/code.rs b/src/syntax/content/parser/token/code.rs index 35a6fd3..a6f6977 100644 --- a/src/syntax/content/parser/token/code.rs +++ b/src/syntax/content/parser/token/code.rs @@ -6,7 +6,7 @@ pub struct Code { } impl Code { - pub fn new(open: bool) -> Code { Code { open } } + pub const fn new(open: bool) -> Code { Code { open } } } impl Parseable for Code { diff --git a/src/syntax/content/parser/token/header.rs b/src/syntax/content/parser/token/header.rs index 88f1643..1f5fe02 100644 --- a/src/syntax/content/parser/token/header.rs +++ b/src/syntax/content/parser/token/header.rs @@ -32,7 +32,7 @@ impl Header { ) -> String { let base_id = if !config.ascii_dom_ids || next_lexeme.next().is_ascii() { - next_lexeme.next().clone() + next_lexeme.next() } else { String::from("h") }; @@ -60,7 +60,7 @@ impl Header { } } - pub fn level(&self) -> u8 { + pub const fn level(&self) -> u8 { match self.level { Level::One => 1, Level::Two => 2, @@ -147,7 +147,7 @@ pub enum Level { } impl Level { - fn from_u8(u: u8) -> Level { + const fn from_u8(u: u8) -> Level { if u <= 1 { Level::One } else if u == 2 { diff --git a/src/syntax/content/parser/token/linebreak.rs b/src/syntax/content/parser/token/linebreak.rs index 25e3d80..023efd7 100644 --- a/src/syntax/content/parser/token/linebreak.rs +++ b/src/syntax/content/parser/token/linebreak.rs @@ -1,7 +1,7 @@ use crate::syntax::content::{Parseable, parser::Lexeme}; #[derive(Default, Debug, Clone, Eq, PartialEq)] -pub struct LineBreak {} +pub struct LineBreak; impl Parseable for LineBreak { fn probe(lexeme: &Lexeme) -> bool { diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs index fa2817e..db7aff1 100644 --- a/src/syntax/content/parser/token/list.rs +++ b/src/syntax/content/parser/token/list.rs @@ -43,19 +43,19 @@ impl Parseable for List { .unwrap_or(0) .strict_div(scale); - output.push_str(&format!("
  • {}", item.text)); + write_log!(output, "
  • {}", item.text); if next_level > level { // open nested lists for _ in 0..(next_level.saturating_sub(level)) { - output.push_str(&format!("<{tag}>\n")); + write_log!(output, "<{tag}>\n"); } } else { // close current item output.push_str("
  • "); // close nested lists for _ in 0..(level.saturating_sub(next_level)) { - output.push_str(&format!("")); + write_log!(output, ""); } output.push('\n'); } @@ -70,7 +70,7 @@ impl Parseable for List { } impl List { - pub fn new(ordered: bool) -> List { + pub const fn new(ordered: bool) -> List { List { ordered, items: vec![], @@ -202,7 +202,7 @@ mod tests { fn token_display() { let list = List::new(false); assert_eq!( - format!("{}", Token::List(list.clone())), + format!("{}", Token::List(list)), "Tk:List [0 unordered items]" ); } diff --git a/src/syntax/content/parser/token/oblique.rs b/src/syntax/content/parser/token/oblique.rs index 124e8fa..fa12ba2 100644 --- a/src/syntax/content/parser/token/oblique.rs +++ b/src/syntax/content/parser/token/oblique.rs @@ -6,7 +6,7 @@ pub struct Oblique { } impl Oblique { - pub fn new(open: bool) -> Oblique { Oblique { open } } + pub const fn new(open: bool) -> Oblique { Oblique { open } } } impl Parseable for Oblique { diff --git a/src/syntax/content/parser/token/paragraph.rs b/src/syntax/content/parser/token/paragraph.rs index 963bfe3..ab635d2 100644 --- a/src/syntax/content/parser/token/paragraph.rs +++ b/src/syntax/content/parser/token/paragraph.rs @@ -6,7 +6,7 @@ pub struct Paragraph { } impl Paragraph { - pub fn new(open: bool) -> Paragraph { Paragraph { open: Some(open) } } + pub const fn new(open: bool) -> Paragraph { Paragraph { open: Some(open) } } pub fn probe_end(lexeme: &Lexeme) -> bool { lexeme.match_char('\n') && lexeme.match_next_char('\n') diff --git a/src/syntax/content/parser/token/preformat.rs b/src/syntax/content/parser/token/preformat.rs index fbddf8a..1ca0128 100644 --- a/src/syntax/content/parser/token/preformat.rs +++ b/src/syntax/content/parser/token/preformat.rs @@ -6,7 +6,7 @@ pub struct PreFormat { } impl PreFormat { - pub fn new(open: bool) -> PreFormat { PreFormat { open: Some(open) } } + pub const fn new(open: bool) -> PreFormat { PreFormat { open: Some(open) } } } impl std::fmt::Display for PreFormat { diff --git a/src/syntax/content/parser/token/strike.rs b/src/syntax/content/parser/token/strike.rs index ae2f9e3..4aadc56 100644 --- a/src/syntax/content/parser/token/strike.rs +++ b/src/syntax/content/parser/token/strike.rs @@ -6,7 +6,7 @@ pub struct Strike { } impl Strike { - pub fn new(open: bool) -> Strike { Strike { open } } + pub const fn new(open: bool) -> Strike { Strike { open } } } impl Parseable for Strike { diff --git a/src/syntax/content/parser/token/underline.rs b/src/syntax/content/parser/token/underline.rs index 1fbbdda..bf60f1d 100644 --- a/src/syntax/content/parser/token/underline.rs +++ b/src/syntax/content/parser/token/underline.rs @@ -6,7 +6,7 @@ pub struct Underline { } impl Underline { - pub fn new(open: bool) -> Underline { Underline { open } } + pub const fn new(open: bool) -> Underline { Underline { open } } } impl Parseable for Underline { diff --git a/src/syntax/content/parser/token/verse.rs b/src/syntax/content/parser/token/verse.rs index b690b57..919929a 100644 --- a/src/syntax/content/parser/token/verse.rs +++ b/src/syntax/content/parser/token/verse.rs @@ -7,7 +7,7 @@ pub struct Verse { } impl Verse { - pub fn new(open: bool) -> Verse { + pub const fn new(open: bool) -> Verse { Verse { open: Some(open), citation: None, From 3ca3f31d8de829f4250b9fbacb6df91561b843ce Mon Sep 17 00:00:00 2001 From: jutty Date: Tue, 17 Feb 2026 21:25:37 -0300 Subject: [PATCH 035/108] Replace CSS prefers-color-scheme block with light-dark() calls --- static/public/assets/style.css | 128 +++++++-------------------------- 1 file changed, 24 insertions(+), 104 deletions(-) diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 519fb8d..8242120 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -1,4 +1,5 @@ :root { + color-scheme: light dark; --base-font-size: 1em; } @@ -15,6 +16,8 @@ body { line-height: 1.75; box-sizing: border-box; min-width: 0; + background: light-dark(#eee, #222); + color: light-dark(#000, #f1e9e5); } main { @@ -61,14 +64,14 @@ code:hover { } pre, code { + background: light-dark(#e0e0e0, #333); font-family: mono, monospace; - background: #e0e0e0; - border: solid 2px #d0d0d0; + border: solid 2px light-dark(#d0d0d0, #434343); border-radius: 6px; } a { - color: #0f6366; + color: light-dark(#0f6366, #1dd7d7); text-decoration: underline dotted #159b9b; text-decoration-thickness: 2.5px; text-underline-offset: 3px; @@ -78,27 +81,27 @@ a { a.attached:hover, #nav-main a:hover { - color: #117c7c; + color: light-dark(#117c7c, #00ffff); text-shadow: 0px 0px 22px #10afaf; transition: 1500ms; } a.detached { - color: #595959; - text-decoration-color: #444444; + color: light-dark(#595959, #acacac); + text-decoration-color: light-dark(#444444, #777); } a.external { - color: #1958a7; - text-decoration-color: #2A7CDF; + color: light-dark(#1958a7, #2fbae4); + text-decoration-color: light-dark(#2A7CDF, #46c1e7); text-decoration-style: solid; text-decoration-thickness: 1.5px; transition: 1500ms; } a.external:hover { - color: #0393b2; - text-decoration-color: #1ed4f1; + color: light-dark(#0393b2, #74e5ff); + text-decoration-color: light-dark(#1ed4f1, #aeffff); transition: 1500ms; } @@ -106,7 +109,7 @@ a:visited, a.detached:visited, a.external:visited { - text-decoration-color: #bbb; + text-decoration-color: light-dark(#bbb, #999); transition: 1500ms; } @@ -143,14 +146,14 @@ span.root-label { span.id-label { font-family: mono, monospace; - background: #e0e0e0; - border: solid 1px #d0d0d0; + background: light-dark(#e0e0e0, #444); + border: solid 1px light-dark(#d0d0d0, #666); } span.hidden-label { - background: #888; - color: #eee; - border: solid 1px #d0d0d0; + background: light-dark(#888, #000); + color: light-dark(#eee, #969696); + border: solid 1px light-dark(#d0d0d0, #555); } h1 { @@ -276,8 +279,8 @@ button border-radius: 5px; padding: 5px 8px; margin-right: 3px; - background: #eeeeff00; - border-color: #216767; + background: light-dark(#eeeeff00, #002020); + border-color: light-dark(#216767, #138e8e); border-width: 0.5px; transition: 1500ms; } @@ -287,7 +290,7 @@ input[type="submit"]:hover, select:hover, button:hover { - border-color: #36a9a9; + border-color: light-dark(#36a9a9, #00ffff); box-shadow: 2px 2px #36a9a9ee; } @@ -315,9 +318,9 @@ table { } table th { - background: #099; + background: light-dark(#099, #002929); color: #fff; - border-color: #222; + border-color: light-dark(#222, #666); } td, th { @@ -362,92 +365,9 @@ p.verse { } @media (prefers-color-scheme: dark) { - * { - background: #222222; - color: #f1e9e5; - } - - pre, code { - background: #333333; - border: solid 1px #434343; - } - - a { - color: #1dd7d7; - transition: 1500ms; - } - - a.attached:hover, - #nav-main a:hover - { - color: #00ffff; - transition: 1500ms; - } - - a.external { - color: #2fbae4; - text-decoration-color: #46c1e7; - transition: 1500ms; - } - - a.external:hover { - color: #74e5ff; - text-decoration-color: #aeffff; - transition: 1500ms; - } - - a.detached { - color: #acacac; - text-decoration-color: #777; - transition: 1500ms; - } - - span.id-label { - background: #444; - border-color: #666; - } - - span.hidden-label { - background: #000; - border-color: #555; - color: #969696; - } - - - a:visited, - a.detached:visited, - a.external:visited - { - text-decoration-color: #999; - transition: 1500ms; - } - - input[type="text"], - input[type="submit"], - select, - button - { - background: #002020; - border-color: #138e8e; - } - - input[type="text"]:hover, - input[type="submit"]:hover, - select:hover, - button:hover - { - border-color: #00ffff; - } - span.root-label { border-width: 1px; } - - table th { - background: #002929; - border-color: #666; - } - } @media (max-width: 600px) { From feb2f9c1ac6b64869047e38bf296a4eebec4db90 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 01:45:11 -0300 Subject: [PATCH 036/108] Add prebuilt binaries tooling and docs --- .forgejo/workflows/publish.yaml | 54 +++++++ .justfile | 39 ++++- Cargo.lock | 205 +++++++++++++------------- src/main.rs | 16 +- src/syntax/command.rs | 32 +++- static/graph.toml | 73 +++++++-- tests/containers/Containerfile.alpine | 14 ++ tests/containers/Containerfile.debian | 15 ++ tests/containers/build.sh | 7 + tests/containers/run.sh | 7 + 10 files changed, 338 insertions(+), 124 deletions(-) create mode 100644 .forgejo/workflows/publish.yaml create mode 100644 tests/containers/Containerfile.alpine create mode 100644 tests/containers/Containerfile.debian create mode 100755 tests/containers/build.sh create mode 100755 tests/containers/run.sh diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml new file mode 100644 index 0000000..56ff89a --- /dev/null +++ b/.forgejo/workflows/publish.yaml @@ -0,0 +1,54 @@ +on: + push: + tags: + - 'v*' +env: + JUST_VERSION: 1.45.0 + JUST_SHA256SUM: dc3f958aaf8c6506dd90426e9b03f86dd15e74a6467ee0e54929f750af3d9e49 + CARGO_LLVM_COV_VERSION: 0.6.21 + CARGO_LLVM_COV_SHA256SUM: 57f491aedf7cdb261538ceb49cbb1ee9d27df7ca205a5e1a009caaf5cb911afb +jobs: + verify: + runs-on: docker + timeout-minutes: 20 + container: + image: rust:slim + steps: + - name: Install action dependencies + run: | + apt-get install --no-install-recommends --update -y nodejs curl + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Rust toolchain + run: | + rustup component add clippy llvm-tools-preview + rustup component add --toolchain nightly rustfmt + + - name: Setup additional tooling + run: | + fetch() { + repo="$1"; tag="$2"; filename="$3"; digest="$4" + + curl -sSLO -w '%{stderr}HTTP %{response_code} %{url}\n' \ + "https://github.com/$repo/releases/download/$tag/$filename" + + printf '%s %s\n' "$digest" "$filename" > digest + sha256sum --check digest && tar xf "$filename" -C tools + } + + mkdir tools + + fetch casey/just ${{ env.JUST_VERSION }} \ + just-${{ env.JUST_VERSION }}-x86_64-unknown-linux-musl.tar.gz \ + ${{ env.JUST_SHA256SUM }} + fetch taiki-e/cargo-llvm-cov v${{ env.CARGO_LLVM_COV_VERSION }} \ + cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz \ + ${{ env.CARGO_LLVM_COV_SHA256SUM }} + + mv -v tools/just tools/cargo-llvm-cov /usr/local/bin + + - name: Build release binary and upload it + run: just upload + diff --git a/.justfile b/.justfile index 9990d42..b5494d3 100644 --- a/.justfile +++ b/.justfile @@ -5,8 +5,6 @@ update: cargo update --verbose -alias u := update - # Build and serve [group: 'develop'] run host='::1' port='3003' *args: @@ -230,10 +228,18 @@ verify: echo "Git working tree is dirty: Commit or stash your changes first" exit 1 fi - {{ just_cmd }} format-assess lint-assess check test cover-assess + {{ just_cmd }} version-assess format-assess lint-assess check test cover-assess alias v := verify +# Check tag-manifest consistency +[script, group: 'assess'] +version-assess: + last_tag=$(git describe --tags --abbrev=0 \ + $(git rev-list --tags --max-count=1) | tr -d v) + manifest_version=$(cat Cargo.toml | grep '^version' | cut -d \" -f 2) + [ "$last_tag" = "$manifest_version" ] + # BUILD # Cleanup build artifacts @@ -259,10 +265,35 @@ alias rb := release-build # Clean, run assessments, release build [group: 'build'] -full-build: clean update verify release-build +full-build: clean release-build alias fb := full-build +# Upload release build to git.jutty.dev package registry +[script, group: 'build'] +upload: full-build && shasum + version=$(./target/release/en --version) + api_root=https://git.jutty.dev/api/ + url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu + file=target/release/en + if [ "${CI:-}" = true ]; then + curl -fsSL \ + --user jutty:${{{{ secrets.GJD_REGISTRY_TOKEN }} \ + --upload-file $file $url + else + curl -fsSL \ + --user jutty:$(secret-tool lookup Title gjd-registry-token) \ + --upload-file $file $url + + fi + +alias u := upload + +# Print sha256sum for CI logging +[group: 'build'] +shasum: + sha256sum target/release/en + ## META [default, private] diff --git a/Cargo.lock b/Cargo.lock index 8423a21..d9c09ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,9 +98,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -129,15 +129,15 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.51" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -151,9 +151,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "num-traits", @@ -279,15 +279,15 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -304,35 +304,35 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", + "slab", ] [[package]] @@ -347,9 +347,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -463,12 +463,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", - "futures-core", "http", "http-body", "hyper", @@ -479,9 +478,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -519,9 +518,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -535,9 +534,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -551,15 +550,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "log" @@ -575,9 +574,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -638,9 +637,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -648,9 +647,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -658,9 +657,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -671,9 +670,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -740,18 +739,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -788,9 +787,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -800,9 +799,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -811,9 +810,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "ring" @@ -831,9 +830,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", @@ -846,18 +845,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -872,9 +871,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -917,9 +916,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -985,9 +984,15 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slug" @@ -1007,9 +1012,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -1023,9 +1028,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -1062,9 +1067,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "libc", "mio", @@ -1087,9 +1092,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", @@ -1111,9 +1116,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -1126,9 +1131,9 @@ checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -1186,9 +1191,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -1204,9 +1209,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" dependencies = [ "base64", "flate2", @@ -1261,9 +1266,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -1274,9 +1279,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1284,9 +1289,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -1297,18 +1302,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -1545,18 +1550,18 @@ checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -1571,6 +1576,6 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zmij" -version = "1.0.0" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/main.rs b/src/main.rs index f561e2d..23fe357 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,8 @@ use en::{ONSET, graph::Graph, log, prelude::*, syntax}; #[tokio::main] #[expect(clippy::print_stderr, clippy::print_stdout, clippy::use_debug)] async fn main() -> io::Result<()> { - log::print_state(); let mut instant = now(); - let args = syntax::command::Arguments::default().parse(); - let address = args.make_address(); - instant = tlog!(&instant, "Parsed CLI arguments"); - panic::set_hook(Box::new(|info| { let payload = info .payload_as_str() @@ -37,12 +32,23 @@ async fn main() -> io::Result<()> { })); instant = tlog!(&instant, "Set up panic hook"); + let args = syntax::command::Arguments::default().parse(); + instant = tlog!(&instant, "Parsed CLI arguments"); + + if args.flags.version { + println!(env!("CARGO_PKG_VERSION")); + return Ok(()); + } + + log::print_state(); + let graph = Graph::load(); instant = tlog!(&instant, "Loaded graph"); let router = en::router::new(graph); tlog!(&instant, "Initialized router"); + let address = args.make_address(); let listener = tokio::net::TcpListener::bind(&address).await.map_err(|e| { log!(ERROR, "Failed to create listener at {address}: {e:#?}"); diff --git a/src/syntax/command.rs b/src/syntax/command.rs index 4d5dd02..04d1195 100644 --- a/src/syntax/command.rs +++ b/src/syntax/command.rs @@ -12,6 +12,12 @@ pub struct Arguments { pub hostname: String, pub port: u16, pub graph_path: PathBuf, + pub flags: Flags, +} + +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct Flags { + pub version: bool, } impl Arguments { @@ -32,6 +38,7 @@ impl Default for Arguments { hostname: String::from("0.0.0.0"), port: 0, graph_path: PathBuf::from("./static/graph.toml"), + flags: Flags::default(), } } } @@ -39,21 +46,33 @@ impl Default for Arguments { fn parse(defaults: &Arguments, args: &[String]) -> Arguments { let mut out_args = defaults.clone(); - let filtered_args = if let Some((head, tail)) = args.split_first() { + // The first argument is usually the command path, but this is not + // guaranteed on all platforms so we drop it if it is unrecognized. + // For now, this is done simply by checking if it starts with a dash + let commandless_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) { + let mut nonflag_args: Vec<&str> = vec![]; + for arg in commandless_args { + if arg == "--version" || arg == "-v" { + out_args.flags.version = true; + } else { + nonflag_args.push(arg); + } + } + + for arg in nonflag_args.chunks(2) { if let Some(argument) = arg.first() && let Some(parameter) = arg.get(1) { - if argument.eq("-h") || argument.eq("--hostname") { - out_args.hostname = String::from(parameter); - } else if argument.eq("-p") || argument.eq("--port") { + if *argument == "-h" || *argument == "--hostname" { + out_args.hostname = String::from(*parameter); + } else if *argument == "-p" || *argument == "--port" { out_args.port = parameter.parse().unwrap_or(out_args.port); - } else if argument.eq("-g") || argument.eq("--graph") { + } else if *argument == "-g" || *argument == "--graph" { out_args.graph_path = PathBuf::from(parameter); } else { if FIRST_PARSE.load(Ordering::SeqCst) { @@ -78,6 +97,7 @@ mod tests { hostname: String::from("localhost"), port: 3007, graph_path: PathBuf::default(), + flags: Flags::default(), }; assert_eq!(args.make_address(), "localhost:3007"); diff --git a/static/graph.toml b/static/graph.toml index daa7c3d..a8007a9 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -4,25 +4,42 @@ root_node = "Documentation" text = """ ## Installation -For now, if you want to try en, you must build it yourself. +### Pre-built binaries -In an environment with a |Rust toolchain|https://rustup.rs/ and Git installed, run: +The easiest way to get started is by downloading a pre-built binary. + +x86-64 Linux binaries are available from the |git.jutty.dev package registry|https://git.jutty.dev/jutty/-/packages/generic/en|. Check the links under "Assets" for direct downloads. + +Other platforms may be supported in the future depending on CI resources. + +### Build from source + +If you are on another platform or simply paranoid, you can also build en yourself. + +You will need: + +- For compiling en, a |Rust toolchain|https://rustup.rs/ +- For compiling dependencies, a C toolchain + +Given the above is satisfied, you can build directly through Cargo: ` -git clone https://codeberg.org/jutty/en -cd en -cargo build --release +cargo install --git https://codeberg.org/jutty/en ` -The en binary will be in `target/release/en`. +And you should now have the `en` command available on your shell. -You can start it and point it to an address, port and graph: +For more information on building from source, see |SourceBuild|. + +## Usage + +Once you have installed en, run it and point it to your graph: ` -en --host localhost --port 3003 --graph ./graph.toml +en --graph my-graph.toml ` -See |CLI| for defaults and details on the CLI options. +See |CLI| for defaults and details on the available options. ## Graph Syntax @@ -87,6 +104,44 @@ This will create a connection from the node with ID `Realism` to a node with ID redirect = "Documentation" hidden = true +[nodes.SourceBuild] +text = """ +Building from source is briefly described in the |Documentation| page. + +Source builds are tested on both Debian and Alpine, meaning en should compile and run on both glibc and musl systems. + +## Dependencies + +A Rust toolchain is required to build en itself and can be installed through |rustup|https://rustup.rs/|. + +For compiling en dependencies, you will need a C compiler and a libc (e.g. `gcc` + `glibc` or `clang` + `musl`), which may already be installed on your system + +% + Distribution ! Needed packages + *Debian* | `gcc` `libc6-dev` + *Alpine* | `clang` +% + +You may also need `curl` or `git` depending on how you will fetch sources. + +## Building from a Git clone + +Aside from the `cargo install` approach described in |Documentation|, ou can alternatively fetch the code yourself first using Git: + +` +git clone https://codeberg.org/jutty/en +cd en +cargo build --release +` + +In this case, the `en` binary will be in `target/release/en`. + +## Runnable examples + +You can find the exact commands used to test installation on both systems in the |tests/containers|https://codeberg.org/jutty/en/src/branch/main/tests/containers| directory of the en source repository. + +""" + [nodes.Node] text = """ A node is defined in your graph file starting with a table header of the form: diff --git a/tests/containers/Containerfile.alpine b/tests/containers/Containerfile.alpine new file mode 100644 index 0000000..d00149a --- /dev/null +++ b/tests/containers/Containerfile.alpine @@ -0,0 +1,14 @@ +FROM alpine:latest +MAINTAINER Juno Takano juno@jutty.dev +USER root + +# Setup tooling +RUN apk add curl clang +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +# Install +RUN echo "root_node = 'test'" > /root/graph.toml +RUN $HOME/.cargo/bin/cargo install --git https://codeberg.org/jutty/en + +# Launch +CMD ["/root/.cargo/bin/en", "-p", "3008", "-g", "/root/graph.toml"] diff --git a/tests/containers/Containerfile.debian b/tests/containers/Containerfile.debian new file mode 100644 index 0000000..0622617 --- /dev/null +++ b/tests/containers/Containerfile.debian @@ -0,0 +1,15 @@ +FROM debian:stable-slim +MAINTAINER Juno Takano juno@jutty.dev +USER root + +# Setup tooling +RUN apt-get -y --update install --no-install-recommends \ + curl ca-certificates gcc libc6-dev +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +# Install +RUN echo "root_node = 'test'" > /root/graph.toml +RUN $HOME/.cargo/bin/cargo install --git https://codeberg.org/jutty/en + +# Launch +CMD ["/root/.cargo/bin/en", "-p", "3008", "-g", "/root/graph.toml"] diff --git a/tests/containers/build.sh b/tests/containers/build.sh new file mode 100755 index 0000000..a59edfb --- /dev/null +++ b/tests/containers/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -eu + +distro="$1" + +podman build --tag "en-$distro" -f "Containerfile.$distro" diff --git a/tests/containers/run.sh b/tests/containers/run.sh new file mode 100755 index 0000000..ff636a1 --- /dev/null +++ b/tests/containers/run.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -eu + +distro="$1" + +podman run --replace --name "en-$distro" --publish 3008:3008 "en-$distro" From 6843a208626779c7e0cede285ee5335746b559cf Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 05:06:36 -0300 Subject: [PATCH 037/108] Add git to publish CI workflow --- .forgejo/workflows/publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 56ff89a..22ae7bb 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -8,7 +8,7 @@ env: CARGO_LLVM_COV_VERSION: 0.6.21 CARGO_LLVM_COV_SHA256SUM: 57f491aedf7cdb261538ceb49cbb1ee9d27df7ca205a5e1a009caaf5cb911afb jobs: - verify: + publish: runs-on: docker timeout-minutes: 20 container: @@ -16,7 +16,7 @@ jobs: steps: - name: Install action dependencies run: | - apt-get install --no-install-recommends --update -y nodejs curl + apt-get install --no-install-recommends --update -y nodejs curl git - name: Checkout code uses: actions/checkout@v6 From a9d7e70f29c853e4c90b860e5264b3d2cab41a44 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 05:09:19 -0300 Subject: [PATCH 038/108] Change check CI workflow clippy to nightly --- .forgejo/workflows/check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index 0cbf49d..12d5cab 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -28,8 +28,8 @@ jobs: - name: Setup Rust toolchain run: | - rustup component add clippy llvm-tools-preview - rustup component add --toolchain nightly rustfmt + rustup component add llvm-tools-preview + rustup component add --toolchain nightly rustfmt clippy - name: Setup additional tooling run: | From edd1dc1016000f85a7cb6c1296bc568b1a35a6d8 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 05:20:23 -0300 Subject: [PATCH 039/108] Move clippy to nightly rustup call on publish workflow --- .forgejo/workflows/publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 22ae7bb..82de69a 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -23,8 +23,8 @@ jobs: - name: Setup Rust toolchain run: | - rustup component add clippy llvm-tools-preview - rustup component add --toolchain nightly rustfmt + rustup component add llvm-tools-preview + rustup component add --toolchain nightly rustfmt clippy - name: Setup additional tooling run: | From c5b6cd05139c2376e45966219f9884194221368a Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 05:22:03 -0300 Subject: [PATCH 040/108] Print git status if worktree is dirty --- .justfile | 1 + Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.justfile b/.justfile index b5494d3..edcde31 100644 --- a/.justfile +++ b/.justfile @@ -226,6 +226,7 @@ verify: export RUSTFLAGS=${RUSTFLAGS:-"-Dwarnings"} if [ -n "$(git status --porcelain)" ]; then echo "Git working tree is dirty: Commit or stash your changes first" + git status exit 1 fi {{ just_cmd }} version-assess format-assess lint-assess check test cover-assess diff --git a/Cargo.lock b/Cargo.lock index d9c09ea..9693d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "en" -version = "0.1.0" +version = "0.1.2" dependencies = [ "axum", "serde", diff --git a/Cargo.toml b/Cargo.toml index fe2cf4a..b3344c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.0" +version = "0.1.2" description = "A non-linear writing instrument." license = "AGPL-3.0-only" From 5303b8dd64084c50192a642329491556e0e4c08e Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 05:27:12 -0300 Subject: [PATCH 041/108] Update dependencies before checking for version mismatch --- .justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.justfile b/.justfile index edcde31..565a8ce 100644 --- a/.justfile +++ b/.justfile @@ -229,7 +229,7 @@ verify: git status exit 1 fi - {{ just_cmd }} version-assess format-assess lint-assess check test cover-assess + {{ just_cmd }} update version-assess format-assess lint-assess check test cover-assess alias v := verify From d8723372ec840a4e3f9626d9f3d6c04ab82aff00 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 15:33:09 -0300 Subject: [PATCH 042/108] CI: Extract tools into /tmp --- .forgejo/workflows/publish.yaml | 12 +++++++----- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 82de69a..121df44 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -31,14 +31,16 @@ jobs: fetch() { repo="$1"; tag="$2"; filename="$3"; digest="$4" - curl -sSLO -w '%{stderr}HTTP %{response_code} %{url}\n' \ + curl -sSLO --output-dir /tmp \ + -w '%{stderr}HTTP %{response_code} %{url}\n' \ "https://github.com/$repo/releases/download/$tag/$filename" - printf '%s %s\n' "$digest" "$filename" > digest - sha256sum --check digest && tar xf "$filename" -C tools + printf '%s %s\n' "$digest" "/tmp/$filename" > /tmp/digest + sha256sum --check /tmp/digest + tar xf "/tmp/$filename" -C /tmp/tools } - mkdir tools + mkdir /tmp/tools fetch casey/just ${{ env.JUST_VERSION }} \ just-${{ env.JUST_VERSION }}-x86_64-unknown-linux-musl.tar.gz \ @@ -47,7 +49,7 @@ jobs: cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz \ ${{ env.CARGO_LLVM_COV_SHA256SUM }} - mv -v tools/just tools/cargo-llvm-cov /usr/local/bin + mv -v /tmp/tools/just /tmp/tools/cargo-llvm-cov /usr/local/bin - name: Build release binary and upload it run: just upload diff --git a/Cargo.lock b/Cargo.lock index 9693d21..82dd28d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" [[package]] name = "bytes" diff --git a/Cargo.toml b/Cargo.toml index b3344c0..83ecc38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.2" +version = "0.1.3" description = "A non-linear writing instrument." license = "AGPL-3.0-only" From 1cd6545c4a5b7ff44784e2d00aebac196c15ea8c Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 16:38:52 -0300 Subject: [PATCH 043/108] Add lockfile version check to version-assess --- .justfile | 5 ++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.justfile b/.justfile index 565a8ce..d2fbf1e 100644 --- a/.justfile +++ b/.justfile @@ -238,8 +238,11 @@ alias v := verify version-assess: last_tag=$(git describe --tags --abbrev=0 \ $(git rev-list --tags --max-count=1) | tr -d v) - manifest_version=$(cat Cargo.toml | grep '^version' | cut -d \" -f 2) + manifest_version=$(grep '^version' Cargo.toml | cut -d \" -f 2) + lockfile_version=$(grep -A 1 'name = "en"' Cargo.lock | + grep version | cut -d '"' -f 2) [ "$last_tag" = "$manifest_version" ] + [ "$last_tag" = "$lockfile_version" ] # BUILD diff --git a/Cargo.lock b/Cargo.lock index 82dd28d..8d55376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "en" -version = "0.1.2" +version = "0.1.5" dependencies = [ "axum", "serde", diff --git a/Cargo.toml b/Cargo.toml index 83ecc38..c81f624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.3" +version = "0.1.5" description = "A non-linear writing instrument." license = "AGPL-3.0-only" From 4bcd2072819c0efcaa28b81a2c8a299fc0fbd0de Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 17:07:26 -0300 Subject: [PATCH 044/108] CI: Extract upload step from justfile --- .forgejo/workflows/publish.yaml | 14 ++++++++++++-- .justfile | 12 +++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 121df44..ae5a935 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -51,6 +51,16 @@ jobs: mv -v /tmp/tools/just /tmp/tools/cargo-llvm-cov /usr/local/bin - - name: Build release binary and upload it - run: just upload + - name: Build release binary + run: just full-build + - name: Upload release binary to git.jutty.dev package registry + run: | + api_root=https://git.jutty.dev/api/ + url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu + curl -fsSL \ + --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ + --upload-file target/release/en $url + + - name: Print sha256sum + run: just shasum diff --git a/.justfile b/.justfile index d2fbf1e..0799a57 100644 --- a/.justfile +++ b/.justfile @@ -280,16 +280,10 @@ upload: full-build && shasum api_root=https://git.jutty.dev/api/ url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu file=target/release/en - if [ "${CI:-}" = true ]; then - curl -fsSL \ - --user jutty:${{{{ secrets.GJD_REGISTRY_TOKEN }} \ - --upload-file $file $url - else - curl -fsSL \ - --user jutty:$(secret-tool lookup Title gjd-registry-token) \ - --upload-file $file $url - fi + curl -fsSL \ + --user jutty:$(secret-tool lookup Title gjd-registry-token) \ + --upload-file $file $url alias u := upload From 4402ad3e4c8ffd1557d4e581b79fc3117be68511 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 17:08:04 -0300 Subject: [PATCH 045/108] Add logging to version consistency check --- .justfile | 12 ++++++++++-- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.justfile b/.justfile index 0799a57..64a65ee 100644 --- a/.justfile +++ b/.justfile @@ -241,8 +241,16 @@ version-assess: manifest_version=$(grep '^version' Cargo.toml | cut -d \" -f 2) lockfile_version=$(grep -A 1 'name = "en"' Cargo.lock | grep version | cut -d '"' -f 2) - [ "$last_tag" = "$manifest_version" ] - [ "$last_tag" = "$lockfile_version" ] + if + [ "$last_tag" != "$manifest_version" ] || + [ "$last_tag" != "$lockfile_version" ] + then + printf 'Last tag: %s\nManifest: %s\nLockfile: %s\n' \ + "$last_tag" "$manifest_version" "$lockfile_version" + exit 1 + fi + +alias va := version-assess # BUILD diff --git a/Cargo.lock b/Cargo.lock index 8d55376..59a26da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "en" -version = "0.1.5" +version = "0.1.6" dependencies = [ "axum", "serde", diff --git a/Cargo.toml b/Cargo.toml index c81f624..2c25b9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.5" +version = "0.1.6" description = "A non-linear writing instrument." license = "AGPL-3.0-only" From 0649cfd94e1f58f9e4cb8620256511fa68d02a64 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 18 Feb 2026 17:27:14 -0300 Subject: [PATCH 046/108] CI: Set version variable for GJD publish URL --- .forgejo/workflows/publish.yaml | 3 ++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index ae5a935..9be60d6 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -53,8 +53,9 @@ jobs: - name: Build release binary run: just full-build - - name: Upload release binary to git.jutty.dev package registry + - name: Publish to git.jutty.dev package registry run: | + version=$(./target/release/en --version) api_root=https://git.jutty.dev/api/ url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu diff --git a/Cargo.lock b/Cargo.lock index 59a26da..2c44a23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "en" -version = "0.1.6" +version = "0.1.7" dependencies = [ "axum", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2c25b9b..e431de5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.6" +version = "0.1.7" description = "A non-linear writing instrument." license = "AGPL-3.0-only" From b794de4f9379363608532be8ab7bba1172a1ef8e Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 1 Mar 2026 04:02:09 -0300 Subject: [PATCH 047/108] Merge ci-testing: publish to GJD registry on tag push commit d6fa2986eca62f79761dffa0ccacdcf6898257e3 Author: jutty Date: Sun Mar 1 03:18:50 2026 -0300 Add tag and push-unsafe recipes to justfile commit 6a239e1708b9b114f2dc37e07541f73f4c6c4522 Author: jutty Date: Sun Mar 1 03:18:21 2026 -0300 Update roadmap commit a3da368573811cf4d7c83ce3d1cdc5fa753da986 Author: jutty Date: Thu Feb 26 20:56:48 2026 -0300 Cleanup CI testing files commit b56f53bdc27b14c8624ad134b292800fce82e12b Author: jutty Date: Thu Feb 26 20:17:10 2026 -0300 CI: Adjust curl logging, add a job for internal networking tests commit 435e478b01742f3c83199e4c9ad4892c2f567898 Author: jutty Date: Wed Feb 25 02:35:20 2026 -0300 CI: Move sha256sum calculation before registry upload commit 727ea16769da9825297b6ce78b9c4e913fe8e87d Author: jutty Date: Wed Feb 25 01:51:05 2026 -0300 CI: Add curl -f fail flag to extra tools binary fetching commit 2ff7a6cf1bdebfa24c2eaea957827b6258dcc0ad Author: jutty Date: Wed Feb 25 01:48:00 2026 -0300 CI: Make additional tooling move to /usr/local/bin verbose commit bf88f86bce124eb83dc6a9ad1a4870490df16835 Author: jutty Date: Wed Feb 25 01:40:32 2026 -0300 CI: Adapt to cargo-audit outlier URL structure commit 291081359ef8ad347bb5586a5f1ff0e83e489f27 Author: jutty Date: Wed Feb 25 01:29:13 2026 -0300 CI: Deduplicate additional tool fetching While this moves the source of truth for CI tooling versions to somewhere outside the workflow definitions, it also avoids duplication and keeps debug (check.yaml) and production (publish.yaml) verifications fully independent. commit 7d2a234fc3993a32e511cc48d46117157313f1fd Author: jutty Date: Wed Feb 25 00:32:51 2026 -0300 Add cargo-audit security assessment commit ed30ee7b75933f5378cda070b9e5b514b58103af Author: jutty Date: Thu Feb 19 02:06:42 2026 -0300 CI: Add wildcard branch to check workflow --- .forgejo/workflows/check.yaml | 32 +++--------------- .forgejo/workflows/publish.yaml | 37 +++------------------ .forgejo/workflows/setup-tools.sh | 42 ++++++++++++++++++++++++ .justfile | 40 +++++++++++++++++++++-- Cargo.lock | 54 +++++++++++++++---------------- Cargo.toml | 2 +- static/graph.toml | 13 +++++--- 7 files changed, 126 insertions(+), 94 deletions(-) create mode 100755 .forgejo/workflows/setup-tools.sh diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index 12d5cab..1bb2eab 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -1,5 +1,7 @@ on: push: + branches: + - '*' paths: - src/** - tests/** @@ -7,11 +9,6 @@ on: - .forgejo/** - Cargo.toml - Cargo.lock -env: - JUST_VERSION: 1.45.0 - JUST_SHA256SUM: dc3f958aaf8c6506dd90426e9b03f86dd15e74a6467ee0e54929f750af3d9e49 - CARGO_LLVM_COV_VERSION: 0.6.21 - CARGO_LLVM_COV_SHA256SUM: 57f491aedf7cdb261538ceb49cbb1ee9d27df7ca205a5e1a009caaf5cb911afb jobs: verify: runs-on: docker @@ -20,8 +17,7 @@ jobs: image: rust:slim steps: - name: Install action dependencies - run: | - apt-get install --no-install-recommends --update -y nodejs curl + run: apt-get install --no-install-recommends --update -y nodejs curl - name: Checkout code uses: actions/checkout@v6 @@ -32,27 +28,7 @@ jobs: rustup component add --toolchain nightly rustfmt clippy - name: Setup additional tooling - run: | - fetch() { - repo="$1"; tag="$2"; filename="$3"; digest="$4" - - curl -sSLO -w '%{stderr}HTTP %{response_code} %{url}\n' \ - "https://github.com/$repo/releases/download/$tag/$filename" - - printf '%s %s\n' "$digest" "$filename" > digest - sha256sum --check digest && tar xf "$filename" -C tools - } - - mkdir tools - - fetch casey/just ${{ env.JUST_VERSION }} \ - just-${{ env.JUST_VERSION }}-x86_64-unknown-linux-musl.tar.gz \ - ${{ env.JUST_SHA256SUM }} - fetch taiki-e/cargo-llvm-cov v${{ env.CARGO_LLVM_COV_VERSION }} \ - cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz \ - ${{ env.CARGO_LLVM_COV_SHA256SUM }} - - mv -v tools/just tools/cargo-llvm-cov /usr/local/bin + run: .forgejo/workflows/setup-tools.sh - name: Build run: just build diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 9be60d6..d4746dc 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -2,11 +2,6 @@ on: push: tags: - 'v*' -env: - JUST_VERSION: 1.45.0 - JUST_SHA256SUM: dc3f958aaf8c6506dd90426e9b03f86dd15e74a6467ee0e54929f750af3d9e49 - CARGO_LLVM_COV_VERSION: 0.6.21 - CARGO_LLVM_COV_SHA256SUM: 57f491aedf7cdb261538ceb49cbb1ee9d27df7ca205a5e1a009caaf5cb911afb jobs: publish: runs-on: docker @@ -27,41 +22,19 @@ jobs: rustup component add --toolchain nightly rustfmt clippy - name: Setup additional tooling - run: | - fetch() { - repo="$1"; tag="$2"; filename="$3"; digest="$4" - - curl -sSLO --output-dir /tmp \ - -w '%{stderr}HTTP %{response_code} %{url}\n' \ - "https://github.com/$repo/releases/download/$tag/$filename" - - printf '%s %s\n' "$digest" "/tmp/$filename" > /tmp/digest - sha256sum --check /tmp/digest - tar xf "/tmp/$filename" -C /tmp/tools - } - - mkdir /tmp/tools - - fetch casey/just ${{ env.JUST_VERSION }} \ - just-${{ env.JUST_VERSION }}-x86_64-unknown-linux-musl.tar.gz \ - ${{ env.JUST_SHA256SUM }} - fetch taiki-e/cargo-llvm-cov v${{ env.CARGO_LLVM_COV_VERSION }} \ - cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz \ - ${{ env.CARGO_LLVM_COV_SHA256SUM }} - - mv -v /tmp/tools/just /tmp/tools/cargo-llvm-cov /usr/local/bin + run: .forgejo/workflows/setup-tools.sh - name: Build release binary run: just full-build + - name: Calculate SHA-256 hash + run: just shasum + - name: Publish to git.jutty.dev package registry run: | version=$(./target/release/en --version) - api_root=https://git.jutty.dev/api/ + api_root=https://git.jutty.dev/api url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu curl -fsSL \ --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ --upload-file target/release/en $url - - - name: Print sha256sum - run: just shasum diff --git a/.forgejo/workflows/setup-tools.sh b/.forgejo/workflows/setup-tools.sh new file mode 100755 index 0000000..cd38738 --- /dev/null +++ b/.forgejo/workflows/setup-tools.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env sh + +set -eu + +JUST_VERSION="1.45.0" +JUST_SHA256SUM="dc3f958aaf8c6506dd90426e9b03f86dd15e74a6467ee0e54929f750af3d9e49" +CARGO_LLVM_COV_VERSION="0.6.21" +CARGO_LLVM_COV_SHA256SUM="57f491aedf7cdb261538ceb49cbb1ee9d27df7ca205a5e1a009caaf5cb911afb" +CARGO_AUDIT_VERSION="0.22.1" +CARGO_AUDIT_TAG="cargo-audit%2Fv$CARGO_AUDIT_VERSION" +CARGO_AUDIT_SHA256SUM="1890badd5f15831a9af4b074399fcd21e6f7c0fe42c84e9254cdffc9f813765c" + +TRIPLE="x86_64-unknown-linux-gnu" +TRIPLE_MUSL="x86_64-unknown-linux-musl" + +fetch() { + repo="$1"; tag="$2"; filename="$3"; digest="$4"; binary="$5" + + [ -d /tmp/tools ] || mkdir -p /tmp/tools + + curl -fsSLO --output-dir /tmp \ + -w '%{stderr}HTTP %{response_code} %{url}\n' \ + "https://github.com/$repo/releases/download/$tag/$filename" + + printf '%s %s\n' "$digest" "/tmp/$filename" > /tmp/digest + sha256sum --check /tmp/digest + tar xf "/tmp/$filename" -C /tmp/tools + find /tmp/tools -type f -executable -name "$binary" \ + -exec mv -v '{}' /usr/local/bin ';' +} + +fetch casey/just "$JUST_VERSION" \ + "just-$JUST_VERSION-$TRIPLE_MUSL.tar.gz" \ + "$JUST_SHA256SUM" just + +fetch taiki-e/cargo-llvm-cov "v$CARGO_LLVM_COV_VERSION" \ + "cargo-llvm-cov-$TRIPLE.tar.gz" \ + "$CARGO_LLVM_COV_SHA256SUM" cargo-llvm-cov + +fetch rustsec/rustsec "$CARGO_AUDIT_TAG" \ + "cargo-audit-$TRIPLE-v$CARGO_AUDIT_VERSION.tgz" \ + "$CARGO_AUDIT_SHA256SUM" cargo-audit diff --git a/.justfile b/.justfile index 64a65ee..66e7370 100644 --- a/.justfile +++ b/.justfile @@ -134,13 +134,43 @@ cover-open: alias oo := cover-open +# Tag HEAD with version from Cargo.toml +[script, group: 'assess'] +tag: update && version-assess + last_tag=$(git describe --tags --abbrev=0 \ + $(git rev-list --tags --max-count=1) | tr -d v) + manifest_version=$(grep '^version' Cargo.toml | cut -d \" -f 2) + lockfile_version=$(grep -A 1 'name = "en"' Cargo.lock | + grep version | cut -d '"' -f 2) + + if [ "$last_tag" = "$manifest_version" ]; then + echo "Last tag $last_tag and manifest ($manifest_version) already match" + exit 1 + elif [ "$manifest_version" != "$lockfile_version" ]; then + echo "Manifest and lockfile versions don't match: update failed?" + exit 1 + fi + + git tag "v$manifest_version" HEAD + # Verify and push [group: 'develop'] push: verify git push + git push --tags alias p := push +# Push without verifying +[group: 'develop'] +push-unsafe: + git push --no-verify + git push --tags --no-verify + +alias pu := push-unsafe + +# DOCUMENT + # Generate crate documentation [group: 'document'] doc: @@ -229,13 +259,14 @@ verify: git status exit 1 fi - {{ just_cmd }} update version-assess format-assess lint-assess check test cover-assess + {{ just_cmd }} update version-assess \ + security-assess format-assess lint-assess check test cover-assess alias v := verify # Check tag-manifest consistency [script, group: 'assess'] -version-assess: +version-assess: update last_tag=$(git describe --tags --abbrev=0 \ $(git rev-list --tags --max-count=1) | tr -d v) manifest_version=$(grep '^version' Cargo.toml | cut -d \" -f 2) @@ -252,6 +283,11 @@ version-assess: alias va := version-assess +# Audit security advisories +security-assess: + cargo audit --deny warnings +alias sa := security-assess + # BUILD # Cleanup build artifacts diff --git a/Cargo.lock b/Cargo.lock index 2c44a23..f47ad70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -151,9 +151,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "en" -version = "0.1.7" +version = "0.1.0" dependencies = [ "axum", "serde", @@ -534,9 +534,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -1028,9 +1028,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1266,9 +1266,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1279,9 +1279,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1289,9 +1289,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1302,9 +1302,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -1550,18 +1550,18 @@ checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e431de5..fe2cf4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.7" +version = "0.1.0" description = "A non-linear writing instrument." license = "AGPL-3.0-only" diff --git a/static/graph.toml b/static/graph.toml index a8007a9..92fc356 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -834,12 +834,17 @@ text = """ - [x] Checkboxes - [x] Move this roadmap to en - [ ] Special sections - - [ ] Definition (implies metadata `has_definition`) - - [ ] See also (implies a kind of connection, e.g. `related`) - - [ ] Not to be confused with (implies a kind of connection) + - [ ] Top-bound + - [ ] Top-bound is not included in the summary (tooltip) + - Sections + - [ ] Definition (implies metadata `has_definition`) + - [ ] See also (implies a kind of connection, e.g. `related`) + - [ ] Not to be confused with (implies a kind of connection) - [ ] Contrast with (implies a kind of connection) - [ ] Example (implies metadata `has_example`) - - [ ] References/influences (implies metadata `has_references`) + - [ ] Bottom-bound + - [ ] References/influences (implies metadata `has_references`) + - [ ] Aggregated from the full text content - [ ] Meta-awareness - [x] Detached edges - [ ] Most linked to nodes From 5377c67b892debef0c6b2cc9682527108ae1bd6b Mon Sep 17 00:00:00 2001 From: jutty Date: Fri, 6 Mar 2026 23:03:57 -0300 Subject: [PATCH 048/108] Add version information to footer, /data and meta tags --- Cargo.lock | 114 ++++++++------------------------------------ src/graph/meta.rs | 3 ++ templates/base.html | 17 ++++++- templates/data.html | 20 ++++++-- 4 files changed, 53 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f47ad70..d99fc08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,9 +748,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1012,12 +1012,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1067,9 +1067,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "libc", "mio", @@ -1081,9 +1081,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -1392,16 +1392,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -1419,31 +1410,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -1452,101 +1426,53 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "zerocopy" diff --git a/src/graph/meta.rs b/src/graph/meta.rs index d94406a..85b6cd8 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -44,6 +44,8 @@ pub struct Config { pub footer_credits: bool, #[serde(default = "mktrue")] pub footer_date: bool, + #[serde(default = "mktrue")] + pub footer_version: bool, #[serde(default)] pub footer_text: String, #[serde(default = "mk8")] @@ -86,6 +88,7 @@ impl Default for Config { footer: true, footer_credits: true, footer_date: true, + footer_version: true, footer_text: String::default(), index_node_count: 8, index_node_list: true, diff --git a/templates/base.html b/templates/base.html index 6e5dfc8..03ebb49 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,3 +1,11 @@ +{% if graph.meta.version %} +{% set en_version = graph.meta.version.major + ~ "." ~ graph.meta.version.minor + ~ "." ~ graph.meta.version.patch %} +{% else %} +{% set en_version = "?.?.?" %} +{% endif -%} + {% if graph.meta.config.content_language %} @@ -12,6 +20,7 @@ {% endif %} + @@ -76,13 +85,17 @@
    {% endif %} {% if graph.meta.config.footer_date %} + built - • - {% endif %} + {% if graph.meta.config.footer_version %} + • + en v{{en_version}} + {% endif %} + {% endif %} diff --git a/templates/data.html b/templates/data.html index ef70757..f7a1bca 100644 --- a/templates/data.html +++ b/templates/data.html @@ -4,8 +4,7 @@ {%- block body %}

    Data

    - -

    Metadata

    +

    Graph insights

    @@ -30,9 +29,19 @@
    -{% if graph.meta.config.raw and (graph.meta.config.raw_toml or graph.meta.config.raw_json) %} -

    Raw formats

    -

    Structured data representing this graph is available in the following formats:

    +

    Metadata

    +

    + This graph was rendered using en {{ en_version }} + (release notes). +

    + +{% if graph.meta.config.raw + and (graph.meta.config.raw_toml + or graph.meta.config.raw_json) +%} +

    Structured data for this graph is available in the following formats:

      {% if graph.meta.config.raw_toml %} @@ -43,4 +52,5 @@ {% endif %}
    {% endif %} + {%- endblock body %} From 75b7cbef8047648da6542426d1978f6fefa19d34 Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 7 Mar 2026 01:21:59 -0300 Subject: [PATCH 049/108] Add 'latest' tag generation, update SourceBuild docs --- .justfile | 54 +++++++++++++++++++++++++++++------------------ Cargo.lock | 2 +- Cargo.toml | 2 +- src/graph/meta.rs | 27 ++++++++++++++++++++++-- static/graph.toml | 16 ++++++++------ 5 files changed, 69 insertions(+), 32 deletions(-) diff --git a/.justfile b/.justfile index 66e7370..44378bd 100644 --- a/.justfile +++ b/.justfile @@ -136,22 +136,23 @@ alias oo := cover-open # Tag HEAD with version from Cargo.toml [script, group: 'assess'] -tag: update && version-assess - last_tag=$(git describe --tags --abbrev=0 \ - $(git rev-list --tags --max-count=1) | tr -d v) - manifest_version=$(grep '^version' Cargo.toml | cut -d \" -f 2) - lockfile_version=$(grep -A 1 'name = "en"' Cargo.lock | - grep version | cut -d '"' -f 2) - - if [ "$last_tag" = "$manifest_version" ]; then - echo "Last tag $last_tag and manifest ($manifest_version) already match" - exit 1 - elif [ "$manifest_version" != "$lockfile_version" ]; then +tag: update + if [ "{{ last_tag }}" = "{{ manifest_version }}" ]; then + echo "Last tag {{ last_tag }} and manifest ({{ manifest_version }}) already match" + if [ "{{ last_tag }}" != "{{ tagged_latest }}" ]; then + echo "Last tag {{ last_tag }} and 'latest' tag ({{ tagged_latest }}) diverge" + git tag latest "v{{ manifest_version }}" + {{ just_cmd }} version-assess + fi + exit + elif [ "{{ manifest_version }}" != "{{ lockfile_version }}" ]; then echo "Manifest and lockfile versions don't match: update failed?" exit 1 fi - git tag "v$manifest_version" HEAD + git tag "v{{ manifest_version }}" HEAD + git tag latest "v{{ manifest_version }}" + {{ just_cmd }} version-assess # Verify and push [group: 'develop'] @@ -267,17 +268,16 @@ alias v := verify # Check tag-manifest consistency [script, group: 'assess'] version-assess: update - last_tag=$(git describe --tags --abbrev=0 \ - $(git rev-list --tags --max-count=1) | tr -d v) - manifest_version=$(grep '^version' Cargo.toml | cut -d \" -f 2) - lockfile_version=$(grep -A 1 'name = "en"' Cargo.lock | - grep version | cut -d '"' -f 2) if - [ "$last_tag" != "$manifest_version" ] || - [ "$last_tag" != "$lockfile_version" ] + [ "{{ last_tag }}" != "{{ tagged_latest }}" ] \ + || [ "{{ last_tag }}" != "{{ lockfile_version }}" ] \ + || [ "{{ last_tag }}" != "{{ lockfile_version }}" ] then - printf 'Last tag: %s\nManifest: %s\nLockfile: %s\n' \ - "$last_tag" "$manifest_version" "$lockfile_version" + printf 'Last tag: %s\nManifest: %s\nLockfile: %s\nTagged latest: %s\n' \ + "{{ last_tag }}" \ + "{{ manifest_version }}" \ + "{{ lockfile_version }}" \ + "{{ tagged_latest }}" exit 1 fi @@ -349,4 +349,16 @@ watch_cmd := "watchexec -qc -r -e rs,toml,html --color always -- " cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|log\.rs"' just_cmd := 'just --timestamp --explain --command-color green' +last_tag := ``` + git tag --sort=-creatordate \ + | grep -v '^latest$' | head -1 | tr -d v + ``` +tagged_latest := `git tag --points-at $(git rev-parse latest) \ + | grep -v '^latest$' | tr -d v` +manifest_version := `grep "^version" Cargo.toml | cut -d \" -f 2` +lockfile_version := ``` + grep -A 1 'name = "en"' Cargo.lock \ + | grep version | cut -d '"' -f 2 + ``` + set unstable diff --git a/Cargo.lock b/Cargo.lock index d99fc08..1b418d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "en" -version = "0.1.0" +version = "0.1.0-alpha" dependencies = [ "axum", "serde", diff --git a/Cargo.toml b/Cargo.toml index fe2cf4a..19bb40c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "en" -version = "0.1.0" +version = "0.1.0-alpha" description = "A non-linear writing instrument." license = "AGPL-3.0-only" diff --git a/src/graph/meta.rs b/src/graph/meta.rs index 85b6cd8..776ff9a 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -117,6 +117,7 @@ pub struct Version { major: u8, minor: u8, patch: u8, + qualifier: Option, } impl Default for Version { @@ -129,6 +130,7 @@ impl Default for Version { major: 0, minor: 0, patch: 0, + qualifier: None, } }, } @@ -137,7 +139,15 @@ impl Default for Version { impl std::fmt::Display for Version { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + if let Some(qualifier) = &self.qualifier { + write!( + f, + "{}.{}.{}-{qualifier}", + self.major, self.minor, self.patch + ) + } else { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } } } @@ -212,7 +222,13 @@ impl Version { let patch: u8 = if triple.len() >= 3 && let Some(s) = triple.get(2) { - match s.trim().parse() { + let patch_number = if let Some(split) = s.split_once('-') { + split.0 + } else { + s + }; + + match patch_number.trim().parse() { Ok(parsed) => parsed, Err(e) => { return Err(VersionError { @@ -228,6 +244,12 @@ impl Version { }); }; + let qualifier: Option = if let Some(tail) = triple.get(2) { + tail.split_once('-').map(|split| String::from(split.1)) + } else { + None + }; + let conditions = has_two_dots && has_three_elements && !has_whitespace @@ -240,6 +262,7 @@ impl Version { major, minor, patch, + qualifier, }) } else { Err(VersionError { diff --git a/static/graph.toml b/static/graph.toml index 92fc356..60440f9 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -18,8 +18,8 @@ If you are on another platform or simply paranoid, you can also build en yoursel You will need: -- For compiling en, a |Rust toolchain|https://rustup.rs/ -- For compiling dependencies, a C toolchain +- For en itself, a |Rust compiler|https://rustup.rs/ +- For dependencies, a C compiler (e.g. `gcc`) Given the above is satisfied, you can build directly through Cargo: @@ -29,7 +29,7 @@ cargo install --git https://codeberg.org/jutty/en And you should now have the `en` command available on your shell. -For more information on building from source, see |SourceBuild|. +For more details on building from source, see |SourceBuild|. ## Usage @@ -106,7 +106,7 @@ hidden = true [nodes.SourceBuild] text = """ -Building from source is briefly described in the |Documentation| page. +An overview on building from source is available in the |Documentation| page. This page contains a more detailed and considered approach for those interested. Source builds are tested on both Debian and Alpine, meaning en should compile and run on both glibc and musl systems. @@ -114,7 +114,9 @@ Source builds are tested on both Debian and Alpine, meaning en should compile an A Rust toolchain is required to build en itself and can be installed through |rustup|https://rustup.rs/|. -For compiling en dependencies, you will need a C compiler and a libc (e.g. `gcc` + `glibc` or `clang` + `musl`), which may already be installed on your system +For compiling en dependencies, you will also need a C toolchain: a compiler and a libc (e.g. `gcc` + `glibc` or `clang` + `musl`), which may already be installed on your system. + +For the two tested systems, all you need are the following packages: % Distribution ! Needed packages @@ -122,11 +124,11 @@ For compiling en dependencies, you will need a C compiler and a libc (e.g. `gcc` *Alpine* | `clang` % -You may also need `curl` or `git` depending on how you will fetch sources. +You may also need `curl`, `git` and `ca-certificates` depending on how you will fetch the source code. ## Building from a Git clone -Aside from the `cargo install` approach described in |Documentation|, ou can alternatively fetch the code yourself first using Git: +Aside from the `cargo install` approach described in |Documentation|, ou can alternatively fetch the code yourself using Git, which allows you to inspect and change it before compiling: ` git clone https://codeberg.org/jutty/en From c0dc8f9b6e1cf0f844cfd08551f1d83a2ae6bfea Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 7 Mar 2026 02:52:58 -0300 Subject: [PATCH 050/108] CI: Also checkout tags, add --locked to cargo builds --- .forgejo/workflows/check.yaml | 2 ++ .forgejo/workflows/publish.yaml | 2 ++ .justfile | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml index 1bb2eab..32a2b61 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -21,6 +21,8 @@ jobs: - name: Checkout code uses: actions/checkout@v6 + with: + fetch-tags: true - name: Setup Rust toolchain run: | diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index d4746dc..532d4e1 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -15,6 +15,8 @@ jobs: - name: Checkout code uses: actions/checkout@v6 + with: + fetch-tags: true - name: Setup Rust toolchain run: | diff --git a/.justfile b/.justfile index 44378bd..8a1a9f2 100644 --- a/.justfile +++ b/.justfile @@ -300,14 +300,14 @@ alias cl := clean # Build project with Cargo [group: 'build'] build: update - cargo build + cargo build --locked alias b := build # Release build [group: 'build'] release-build: update verify - cargo build --release + cargo build --locked --release alias rb := release-build From 54799bacc408d6bd2b758e2239e96af6b20d9e14 Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 7 Mar 2026 02:58:13 -0300 Subject: [PATCH 051/108] CI: Add Git to check.yaml workflow to suppress justfile errors --- .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 32a2b61..eaa81a0 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/check.yaml @@ -17,7 +17,7 @@ jobs: image: rust:slim steps: - name: Install action dependencies - run: apt-get install --no-install-recommends --update -y nodejs curl + run: apt-get install --no-install-recommends --update -y nodejs curl git - name: Checkout code uses: actions/checkout@v6 From 6c6e9a3f7e9ed5881078ceba824aa9df0c271d6c Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 7 Mar 2026 15:08:16 -0300 Subject: [PATCH 052/108] Fix test containers failing to respond --- tests/containers/Containerfile.alpine | 19 ++++++++++++++----- tests/containers/Containerfile.debian | 19 ++++++++++++++----- tests/containers/README.md | 2 ++ tests/containers/build.sh | 6 ++++-- tests/containers/run.sh | 7 +++++-- 5 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 tests/containers/README.md diff --git a/tests/containers/Containerfile.alpine b/tests/containers/Containerfile.alpine index d00149a..784f3c3 100644 --- a/tests/containers/Containerfile.alpine +++ b/tests/containers/Containerfile.alpine @@ -1,14 +1,23 @@ FROM alpine:latest MAINTAINER Juno Takano juno@jutty.dev -USER root +ENV DEBUG=debug +ENV TAG=${TAG:-latest} # Setup tooling -RUN apk add curl clang +RUN apk add curl clang git file RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Install -RUN echo "root_node = 'test'" > /root/graph.toml -RUN $HOME/.cargo/bin/cargo install --git https://codeberg.org/jutty/en +RUN git clone -b "$TAG" --single-branch https://codeberg.org/jutty/en /build +RUN < /root/graph.toml -RUN $HOME/.cargo/bin/cargo install --git https://codeberg.org/jutty/en +RUN git clone -b "$TAG" --single-branch https://codeberg.org/jutty/en /build +RUN < Date: Sat, 7 Mar 2026 17:28:34 -0300 Subject: [PATCH 053/108] Fix missing semver qualifier in templates --- templates/base.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/templates/base.html b/templates/base.html index 03ebb49..75b7e20 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,9 +1,14 @@ {% if graph.meta.version %} -{% set en_version = graph.meta.version.major - ~ "." ~ graph.meta.version.minor - ~ "." ~ graph.meta.version.patch %} + {% set en_version = + graph.meta.version.major + ~ "." ~ graph.meta.version.minor + ~ "." ~ graph.meta.version.patch + %} + {% if graph.meta.version.qualifier %} + {% set en_version = en_version ~ "-" ~ graph.meta.version.qualifier %} + {% endif -%} {% else %} -{% set en_version = "?.?.?" %} + {% set en_version = "?.?.?" %} {% endif -%} From d9341e76866eda46691ed9c70ba4dc0c4c15b65e Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 7 Mar 2026 17:28:47 -0300 Subject: [PATCH 054/108] Add dev containers, musl build --- .forgejo/workflows/publish.yaml | 32 +++++++++++++++----- .justfile | 36 ++++++++++------------- tests/containers/Containerfile.alpine-dev | 13 ++++++++ tests/containers/Containerfile.debian-dev | 13 ++++++++ tests/containers/build.sh | 20 ++++++++++--- tests/containers/run.sh | 8 +++-- 6 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 tests/containers/Containerfile.alpine-dev create mode 100644 tests/containers/Containerfile.debian-dev diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 532d4e1..833a7d6 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -22,21 +22,37 @@ jobs: run: | rustup component add llvm-tools-preview rustup component add --toolchain nightly rustfmt clippy + rustup target add x86_64-unknown-linux-musl - name: Setup additional tooling run: .forgejo/workflows/setup-tools.sh - - name: Build release binary - run: just full-build - - name: Calculate SHA-256 hash + - name: Build x64 glibc release binary + run: just full-build x86_64-unknown-linux-gnu + + - name: Build x64 musl release binary + run: just full-build x86_64-unknown-linux-musl + + - name: Calculate SHA-256 hashes run: just shasum - - name: Publish to git.jutty.dev package registry + - name: Publish x64 glibc binary to git.jutty.dev registry run: | - version=$(./target/release/en --version) + version=$(./target/x86_64-unknown-linux-gnu/en --version) api_root=https://git.jutty.dev/api - url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu + url=$api_root/packages/jutty/generic/en/$version/en-x64-linux-gnu curl -fsSL \ - --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ - --upload-file target/release/en $url + --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ + --upload-file target/x86_64-unknown-linux-gnu/en $url + + - name: Publish x64 musl binary to git.jutty.dev registry + run: | + version=$(./target/x86_64-unknown-linux-musl/en --version) + api_root=https://git.jutty.dev/api + url=$api_root/packages/jutty/generic/en/$version/en-x64-linux-musl + + curl -fsSL \ + --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ + --upload-file target/x86_64-unknown-linux-musl/en $url + diff --git a/.justfile b/.justfile index 8a1a9f2..866cb13 100644 --- a/.justfile +++ b/.justfile @@ -299,42 +299,30 @@ alias cl := clean # Build project with Cargo [group: 'build'] -build: update - cargo build --locked +build target=default_target: update + cargo build --target {{ target }} --locked alias b := build # Release build [group: 'build'] -release-build: update verify - cargo build --locked --release +release-build target=default_target: update verify + cargo build --target {{ target }} --locked --release alias rb := release-build # Clean, run assessments, release build [group: 'build'] -full-build: clean release-build +full-build target: clean (release-build target) alias fb := full-build -# Upload release build to git.jutty.dev package registry -[script, group: 'build'] -upload: full-build && shasum - version=$(./target/release/en --version) - api_root=https://git.jutty.dev/api/ - url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu - file=target/release/en - - curl -fsSL \ - --user jutty:$(secret-tool lookup Title gjd-registry-token) \ - --upload-file $file $url - -alias u := upload - -# Print sha256sum for CI logging +# Calculate SHA 256 hashes for release binaries [group: 'build'] shasum: - sha256sum target/release/en + find target -type d -name 'release' \ + -exec find '{}' -maxdepth 1 -type f -name en -executable ';' \ + | xargs sha256sum ## META @@ -342,8 +330,14 @@ shasum: default: @just --list --unsorted --justfile {{justfile()}} +choose: + @just --choose + +alias ch := choose + export CARGO_TERM_COLOR := 'always' +default_target := "x86_64-unknown-linux-gnu" debug_vars := 'DEBUG=${DEBUG:-} DEBUG_FILTER=${DEBUG_FILTER:-} RUST_BACKTRACE=${RUST_BACKTRACE:-} RUSTFLAGS=${RUSTFLAGS:-}' watch_cmd := "watchexec -qc -r -e rs,toml,html --color always -- " cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|log\.rs"' diff --git a/tests/containers/Containerfile.alpine-dev b/tests/containers/Containerfile.alpine-dev new file mode 100644 index 0000000..d6a7e45 --- /dev/null +++ b/tests/containers/Containerfile.alpine-dev @@ -0,0 +1,13 @@ +FROM docker.io/library/rust:alpine +MAINTAINER Juno Takano juno@jutty.dev +ENV DEBUG=debug + +# Install +COPY en /usr/local/bin/en + +# Describe +RUN sha256sum $(which en) + +# Launch +WORKDIR /root +CMD ["en", "-p", "80"] diff --git a/tests/containers/Containerfile.debian-dev b/tests/containers/Containerfile.debian-dev new file mode 100644 index 0000000..95372cc --- /dev/null +++ b/tests/containers/Containerfile.debian-dev @@ -0,0 +1,13 @@ +FROM debian:stable-slim +MAINTAINER Juno Takano juno@jutty.dev +ENV DEBUG=debug + +# Install +COPY en /usr/local/bin/en + +# Describe +RUN sha256sum $(which en) + +# Launch +WORKDIR /root +CMD ["en", "-p", "80"] diff --git a/tests/containers/build.sh b/tests/containers/build.sh index df4aa02..f426ea2 100755 --- a/tests/containers/build.sh +++ b/tests/containers/build.sh @@ -1,9 +1,21 @@ #!/usr/bin/env sh set -eu -distro="$1" +suffix=$(printf '%s' "$1" | sed 's/.*\.//') +tag="en:$suffix" + +if podman container exists "$tag"; then + podman stop --time 3 "$tag" +fi + +if [ "$suffix" = 'debian-dev' ]; then + cp ../../target/release/en en +elif [ "$suffix" = 'alpine-dev' ]; then + cp ../../target/x86_64-unknown-linux-musl/release/en en +fi -podman stop --time 3 "en-$distro" podman build \ - --tag "en-$distro" \ - -f "Containerfile.$distro" + --tag "$tag" \ + -f "Containerfile.$suffix" + +rm en diff --git a/tests/containers/run.sh b/tests/containers/run.sh index 5a3efd5..784ec44 100755 --- a/tests/containers/run.sh +++ b/tests/containers/run.sh @@ -1,10 +1,12 @@ #!/usr/bin/env sh set -eu -distro="$1" +suffix=$(printf '%s' "$1" | sed 's/.*\.//') +name="en-$suffix" +tag="en:$suffix" podman run \ --replace \ - --name "en-$distro" \ + --name "$name" \ --publish 3008:80 \ - "en-$distro" + "$tag" From d246c7c598b6bf6c6ecab202d8318c6ac4b0df89 Mon Sep 17 00:00:00 2001 From: jutty Date: Sat, 7 Mar 2026 17:28:47 -0300 Subject: [PATCH 055/108] Add dev containers, musl build --- .forgejo/workflows/publish.yaml | 35 ++++++++++--- .../workflows/{check.yaml => verify.yaml} | 2 + .justfile | 49 ++++++++++--------- .../Containerfile.alpine | 0 containers/Containerfile.alpine-dev | 13 +++++ .../Containerfile.debian | 0 containers/Containerfile.debian-dev | 13 +++++ {tests/containers => containers}/README.md | 0 containers/build.sh | 23 +++++++++ containers/run.sh | 12 +++++ tests/containers/build.sh | 9 ---- tests/containers/run.sh | 10 ---- 12 files changed, 117 insertions(+), 49 deletions(-) rename .forgejo/workflows/{check.yaml => verify.yaml} (90%) rename {tests/containers => containers}/Containerfile.alpine (100%) create mode 100644 containers/Containerfile.alpine-dev rename {tests/containers => containers}/Containerfile.debian (100%) create mode 100644 containers/Containerfile.debian-dev rename {tests/containers => containers}/README.md (100%) create mode 100755 containers/build.sh create mode 100755 containers/run.sh delete mode 100755 tests/containers/build.sh delete mode 100755 tests/containers/run.sh diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index 532d4e1..7d9686d 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -22,21 +22,40 @@ jobs: run: | rustup component add llvm-tools-preview rustup component add --toolchain nightly rustfmt clippy + rustup target add x86_64-unknown-linux-musl - name: Setup additional tooling run: .forgejo/workflows/setup-tools.sh - - name: Build release binary - run: just full-build - - name: Calculate SHA-256 hash + - name: Run all assessments + run: just verify + + - name: Build x64 glibc release binary + run: just release-build x86_64-unknown-linux-gnu + + - name: Build x64 musl release binary + run: just release-build x86_64-unknown-linux-musl + + - name: Calculate SHA-256 hashes run: just shasum - - name: Publish to git.jutty.dev package registry + - name: Publish x64 glibc binary to git.jutty.dev registry run: | - version=$(./target/release/en --version) + version=$(./target/x86_64-unknown-linux-gnu/release/en --version) api_root=https://git.jutty.dev/api - url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu + url=$api_root/packages/jutty/generic/en/$version/en-x64-linux-gnu curl -fsSL \ - --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ - --upload-file target/release/en $url + --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ + --upload-file target/x86_64-unknown-linux-gnu/release/en $url + + - name: Publish x64 musl binary to git.jutty.dev registry + run: | + version=$(./target/x86_64-unknown-linux-musl/release/en --version) + api_root=https://git.jutty.dev/api + url=$api_root/packages/jutty/generic/en/$version/en-x64-linux-musl + + curl -fsSL \ + --user jutty:${{ secrets.GJD_REGISTRY_TOKEN }} \ + --upload-file target/x86_64-unknown-linux-musl/release/en $url + diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/verify.yaml similarity index 90% rename from .forgejo/workflows/check.yaml rename to .forgejo/workflows/verify.yaml index eaa81a0..a8ebe71 100644 --- a/.forgejo/workflows/check.yaml +++ b/.forgejo/workflows/verify.yaml @@ -28,6 +28,8 @@ jobs: run: | rustup component add llvm-tools-preview rustup component add --toolchain nightly rustfmt clippy + rustup target add x86_64-unknown-linux-gnu + rustup target add x86_64-unknown-linux-musl - name: Setup additional tooling run: .forgejo/workflows/setup-tools.sh diff --git a/.justfile b/.justfile index 8a1a9f2..62f4a15 100644 --- a/.justfile +++ b/.justfile @@ -260,7 +260,7 @@ verify: git status exit 1 fi - {{ just_cmd }} update version-assess \ + {{ just_cmd }} version-assess \ security-assess format-assess lint-assess check test cover-assess alias v := verify @@ -299,42 +299,38 @@ alias cl := clean # Build project with Cargo [group: 'build'] -build: update - cargo build --locked +build target=default_target: + cargo build --target {{ target }} --locked alias b := build # Release build [group: 'build'] -release-build: update verify - cargo build --locked --release +release-build target=default_target: + cargo build --target {{ target }} --locked --release alias rb := release-build -# Clean, run assessments, release build +# glibc release build [group: 'build'] -full-build: clean release-build +release-build-gnu: + cargo build --target {{ glibc_target }} --locked --release -alias fb := full-build +alias rbg := release-build-gnu -# Upload release build to git.jutty.dev package registry -[script, group: 'build'] -upload: full-build && shasum - version=$(./target/release/en --version) - api_root=https://git.jutty.dev/api/ - url=$api_root/packages/jutty/generic/en/$version/en-x86_64-linux-gnu - file=target/release/en +# musl release build +[group: 'build'] +release-build-musl: + cargo build --target {{ musl_target }} --locked --release - curl -fsSL \ - --user jutty:$(secret-tool lookup Title gjd-registry-token) \ - --upload-file $file $url +alias rbm := release-build-musl -alias u := upload - -# Print sha256sum for CI logging +# Calculate SHA 256 hashes for release binaries [group: 'build'] shasum: - sha256sum target/release/en + find target -type d -name 'release' \ + -exec find '{}' -maxdepth 1 -type f -name en -executable ';' \ + | xargs sha256sum ## META @@ -342,8 +338,17 @@ shasum: default: @just --list --unsorted --justfile {{justfile()}} +choose: + @just --choose + +alias ch := choose + export CARGO_TERM_COLOR := 'always' +musl_target := "x86_64-unknown-linux-musl" +glibc_target := "x86_64-unknown-linux-gnu" +default_target := musl_target + debug_vars := 'DEBUG=${DEBUG:-} DEBUG_FILTER=${DEBUG_FILTER:-} RUST_BACKTRACE=${RUST_BACKTRACE:-} RUSTFLAGS=${RUSTFLAGS:-}' watch_cmd := "watchexec -qc -r -e rs,toml,html --color always -- " cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|log\.rs"' diff --git a/tests/containers/Containerfile.alpine b/containers/Containerfile.alpine similarity index 100% rename from tests/containers/Containerfile.alpine rename to containers/Containerfile.alpine diff --git a/containers/Containerfile.alpine-dev b/containers/Containerfile.alpine-dev new file mode 100644 index 0000000..d6a7e45 --- /dev/null +++ b/containers/Containerfile.alpine-dev @@ -0,0 +1,13 @@ +FROM docker.io/library/rust:alpine +MAINTAINER Juno Takano juno@jutty.dev +ENV DEBUG=debug + +# Install +COPY en /usr/local/bin/en + +# Describe +RUN sha256sum $(which en) + +# Launch +WORKDIR /root +CMD ["en", "-p", "80"] diff --git a/tests/containers/Containerfile.debian b/containers/Containerfile.debian similarity index 100% rename from tests/containers/Containerfile.debian rename to containers/Containerfile.debian diff --git a/containers/Containerfile.debian-dev b/containers/Containerfile.debian-dev new file mode 100644 index 0000000..95372cc --- /dev/null +++ b/containers/Containerfile.debian-dev @@ -0,0 +1,13 @@ +FROM debian:stable-slim +MAINTAINER Juno Takano juno@jutty.dev +ENV DEBUG=debug + +# Install +COPY en /usr/local/bin/en + +# Describe +RUN sha256sum $(which en) + +# Launch +WORKDIR /root +CMD ["en", "-p", "80"] diff --git a/tests/containers/README.md b/containers/README.md similarity index 100% rename from tests/containers/README.md rename to containers/README.md diff --git a/containers/build.sh b/containers/build.sh new file mode 100755 index 0000000..6e2338d --- /dev/null +++ b/containers/build.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +set -eu +suffix=$(printf '%s' "$1" | sed 's/.*\.//') +tag="en:$suffix" + +if podman container exists "$tag"; then + podman stop --time 3 "$tag" +fi + +if [ "$suffix" = 'debian-dev' ]; then + cp -v ../target/x86_64-unknown-linux-gnu/release/en en +elif [ "$suffix" = 'alpine-dev' ]; then + cp -v ../target/x86_64-unknown-linux-musl/release/en en +fi + +podman build \ + --tag "$tag" \ + -f "Containerfile.$suffix" + +if [ -f en ]; then + rm -v en +fi diff --git a/containers/run.sh b/containers/run.sh new file mode 100755 index 0000000..784ec44 --- /dev/null +++ b/containers/run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +set -eu +suffix=$(printf '%s' "$1" | sed 's/.*\.//') +name="en-$suffix" +tag="en:$suffix" + +podman run \ + --replace \ + --name "$name" \ + --publish 3008:80 \ + "$tag" diff --git a/tests/containers/build.sh b/tests/containers/build.sh deleted file mode 100755 index df4aa02..0000000 --- a/tests/containers/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env sh - -set -eu -distro="$1" - -podman stop --time 3 "en-$distro" -podman build \ - --tag "en-$distro" \ - -f "Containerfile.$distro" diff --git a/tests/containers/run.sh b/tests/containers/run.sh deleted file mode 100755 index 5a3efd5..0000000 --- a/tests/containers/run.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh - -set -eu -distro="$1" - -podman run \ - --replace \ - --name "en-$distro" \ - --publish 3008:80 \ - "en-$distro" From 56a9541fdf6cee656137a5af8dd64df3bba34f36 Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Mar 2026 02:53:04 -0300 Subject: [PATCH 056/108] Use emergency_wrap in more cases --- src/router/handlers/template.rs | 77 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index 1a5f520..72c1fb2 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -53,7 +53,7 @@ pub(in crate::router::handlers) fn render( let tera = match tera::Tera::new("./templates/**/*") { Ok(t) => t, Err(e) => { - return (emergency_wrap(&e), 500); + return (emergency_wrap(&e, "Failed instantiating template engine"), 500); }, }; @@ -66,15 +66,8 @@ pub(in crate::router::handlers) fn render( let mut error_context = tera::Context::default(); let mut out_error_message = match error_message { - Some(s) => format!( - "Template render failed.\n\ - User message: {s}, - Engine message:\n
    {e:#?}
    " - ), - None => format!( - "Template render failed.\n\ - Engine message:\n
    {e:#?}
    " - ), + Some(s) => emergency_wrap(&e, &s), + None => emergency_wrap(&e, "Template render failed."), }; if log::env_level() >= VERBOSE { @@ -100,36 +93,42 @@ pub(in crate::router::handlers) fn render( } } -fn emergency_wrap(error: &tera::Error) -> String { +fn emergency_wrap(error: &tera::Error, message: &str) -> String { log!(ERROR, "{error:#?}"); + + let message_element = format!("

    {message}

    "); + format!( - r#" - - - Pre-Templating Error - - - - - -

    Early Pre-Templating Error

    -

    This normally indicates a malformed template.

    -
    -            {error:#?}
    -            
    -

    - If you haven't modified templates, plese consider - reporting it. -

    - - - "# + "\n\ + \n\ + \n\ + en Pre-Templating Error\n + \n\ + \n\ + \n\ + \n\ + \n\ +

    en Early Pre-Templating Error

    \n\ + {message_element}\n\ +
    \n\
    +            {error:#?}\n\
    +            
    \n\ +

    This normally indicates a malformed or missing template.

    \n\ +

    \n\ + If you haven't modified templates, please consider\n\ + reporting it.\n\ +

    \n\ + \n\ + \n\ + " ) } @@ -229,7 +228,7 @@ mod tests { fn emergency_wrap_custom_message() { let payload = "JLaTtsnd2IFukIOvqFNymeuiaS6nMaUc"; let error = tera::Error::msg(payload); - let html = emergency_wrap(&error); + let html = emergency_wrap(&error, ""); assert!(html.matches(payload).count() == 1); } } From 2a4165e9e03fac7fc000275b3d9a2280463a32be Mon Sep 17 00:00:00 2001 From: jutty Date: Sun, 8 Mar 2026 23:17:14 -0300 Subject: [PATCH 057/108] Better templating error message, container builds justfile recipes --- .justfile | 48 +++++++++++++++++++++++++++++++-- Cargo.lock | 4 +-- containers/build.sh | 4 +-- src/router/handlers/template.rs | 29 ++++++++++++++------ static/graph.toml | 12 +++++---- 5 files changed, 78 insertions(+), 19 deletions(-) diff --git a/.justfile b/.justfile index 62f4a15..54eeb91 100644 --- a/.justfile +++ b/.justfile @@ -20,6 +20,35 @@ run-watch: alias w := run-watch +# Build on changes +[group: 'develop'] +build-watch target=default_target: + @{{ watch_cmd }} {{ just_cmd }} build {{ target }} + +alias bw := build-watch + +# Build dev container +[group: 'develop', working-directory: 'containers'] +build-containerized distro="alpine": build + ./build.sh {{ distro }}-dev + +alias bc := build-containerized + +# Run dev container +[group: 'develop', working-directory: 'containers'] +run-containerized distro="alpine": + ./run.sh {{ distro }}-dev + +alias rc := run-containerized + +# Build dev container and serve from it on changes +[group: 'develop'] +run-watch-containerized: + @{{ watch_cmd }} "{{ just_cmd }} build-containerized \ + && {{ just_cmd }} run-containerized" + +alias wc := run-watch-containerized + [private] quick-assess: {{ just_cmd }} lint check quick-test-cover @@ -297,13 +326,28 @@ clean: alias cl := clean -# Build project with Cargo +# Build [group: 'build'] build target=default_target: cargo build --target {{ target }} --locked + alias b := build +# glibc build +[group: 'build'] +build-gnu: + cargo build --target {{ glibc_target }} --locked + +alias bg := build-gnu + +# musl build +[group: 'build'] +build-musl: + cargo build --target {{ musl_target }} --locked + +alias bm := build-musl + # Release build [group: 'build'] release-build target=default_target: @@ -314,7 +358,7 @@ alias rb := release-build # glibc release build [group: 'build'] release-build-gnu: - cargo build --target {{ glibc_target }} --locked --release + cargo build --target {{ glibc_target }} --locked --release alias rbg := release-build-gnu diff --git a/Cargo.lock b/Cargo.lock index 1b418d6..cad448a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,9 +550,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" diff --git a/containers/build.sh b/containers/build.sh index 6e2338d..33a5816 100755 --- a/containers/build.sh +++ b/containers/build.sh @@ -9,9 +9,9 @@ if podman container exists "$tag"; then fi if [ "$suffix" = 'debian-dev' ]; then - cp -v ../target/x86_64-unknown-linux-gnu/release/en en + cp -v ../target/x86_64-unknown-linux-gnu/debug/en en elif [ "$suffix" = 'alpine-dev' ]; then - cp -v ../target/x86_64-unknown-linux-musl/release/en en + cp -v ../target/x86_64-unknown-linux-musl/debug/en en fi podman build \ diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index 72c1fb2..92a8041 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -53,7 +53,10 @@ pub(in crate::router::handlers) fn render( let tera = match tera::Tera::new("./templates/**/*") { Ok(t) => t, Err(e) => { - return (emergency_wrap(&e, "Failed instantiating template engine"), 500); + return ( + emergency_wrap(&e, "Failed instantiating template engine"), + 500, + ); }, }; @@ -103,8 +106,11 @@ fn emergency_wrap(error: &tera::Error, message: &str) -> String { \n\ \n\ en Pre-Templating Error\n - \n\ - \n\ + \n\ + \n\