Merge serial module into the graph module

This commit is contained in:
Juno Takano 2026-01-13 12:11:51 -03:00
commit 697dcc720d
17 changed files with 421 additions and 332 deletions

View file

@ -15,10 +15,16 @@ type Probe = fn(&Lexeme) -> bool;
type Lexer = fn(&Lexeme) -> Token;
type LexMap<'lm> = &'lm [(Probe, Lexer)];
pub struct TokenOutput {
pub text: Option<String>,
pub tokens: Vec<Token>,
pub format_tokens: Vec<Token>,
}
pub fn parse(text: &str, graph: &Graph) -> String {
parser::read(text, graph)
}
pub fn rich_parse(text: &str, graph: &Graph) -> (String, Vec<Token>) {
pub fn rich_parse(text: &str, graph: &Graph) -> TokenOutput {
parser::rich_read(text, graph)
}

View file

@ -1,5 +1,5 @@
use crate::{prelude::*, graph::Graph};
use super::{Parseable as _, LexMap};
use super::{TokenOutput, Parseable as _, LexMap};
use token::{LineBreak, Literal};
use context::{Block, Inline};
pub use {lexeme::Lexeme, token::Token, state::State};
@ -20,7 +20,7 @@ const LEXMAP: LexMap = &[
}),
];
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> Vec<Token> {
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput {
let mut tokens: Vec<Token> = Vec::default();
let mut state = State::default();
@ -75,27 +75,37 @@ fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> Vec<Token> {
}
context::close(&state, &mut tokens);
tokens
TokenOutput {
tokens,
format_tokens: state.format_tokens,
text: None,
}
}
pub(super) fn read(input: &str, graph: &Graph) -> String {
parse(&lex(input, LEXMAP, graph, true))
parse(&lex(input, LEXMAP, graph, true).tokens)
}
pub(super) fn rich_read(input: &str, graph: &Graph) -> (String, Vec<Token>) {
let tokens = lex(input, LEXMAP, graph, true);
let text = parse(&tokens);
(text, tokens)
pub(super) fn rich_read(input: &str, graph: &Graph) -> TokenOutput {
let lex_output = lex(input, LEXMAP, graph, true);
let text = parse(&lex_output.tokens);
TokenOutput {
text: Some(text),
tokens: lex_output.tokens,
format_tokens: lex_output.format_tokens,
}
}
/// Apply end-to-end point and inline parsing for nested formatting, such as
/// inside the display text of anchors and list items
pub fn format(input: &str, graph: &Graph) -> String {
parse(&lex(input, LEXMAP, graph, false))
pub fn format(input: &str, graph: &Graph) -> (String, Vec<Token>) {
let tokens = lex(input, LEXMAP, graph, false).tokens;
(parse(&tokens), tokens)
}
// Strip special syntax for display in noninteractive or plain-text display
pub fn flatten(input: &str, graph: &Graph) -> String {
let tokens = lex(input, LEXMAP, graph, true);
let tokens = lex(input, LEXMAP, graph, true).tokens;
let flat = tokens.iter().map(Token::flatten).collect::<String>();
log!("Flattened {tokens:?} to {flat}");
flat

View file

@ -67,6 +67,9 @@ pub fn parse(
} else if lexeme.match_char('|') && lexeme.is_next_delimiter() {
log!("End: Pipe followed by delimiter");
if buffer.destination.is_empty() {
if candidate.text().contains(':') {
candidate.set_external(true);
}
push(Some(&candidate.text().clone()), tokens, state, graph);
} else {
push(Some(&buffer.destination.clone()), tokens, state, graph);

View file

@ -48,7 +48,10 @@ pub fn parse(
}
if item_candidate.depth.is_some() {
// if the current item candidate has a known depth, push it
item_candidate.text = format(&item_candidate.text, graph);
let (text, format_tokens) =
format(&item_candidate.text, graph);
item_candidate.text = text;
state.format_tokens.extend_from_slice(&format_tokens);
candidate.items.push(item_candidate.clone());
}
// push list candidate, reset state and exit context
@ -60,7 +63,9 @@ pub fn parse(
} else if lexeme.match_char('\n') {
// found end of item, push it and reset state
log!("Accepting item candidate {item_candidate}");
item_candidate.text = format(&item_candidate.text, graph);
let (text, format_tokens) = format(&item_candidate.text, graph);
item_candidate.text = text;
state.format_tokens.extend_from_slice(&format_tokens);
candidate.items.push(item_candidate.clone());
*item_candidate = Item::default();
buffer.depth = 0;

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use crate::syntax::content::parser::{
Token,
context::{Block, Context, Inline},
token::{Anchor, Item, List},
};
@ -11,6 +12,7 @@ pub struct State {
pub dom_ids: HashMap<String, Vec<String>>,
pub switches: Switches,
pub buffers: Buffers,
pub format_tokens: Vec<Token>,
}
#[derive(Clone, Debug)]
@ -91,6 +93,7 @@ impl Default for State {
depth: 0,
},
},
format_tokens: vec![],
}
}
}

View file

@ -1,275 +0,0 @@
use std::collections::HashMap;
use crate::{
syntax::{
command::Arguments,
content::{
self,
parser::{flatten, Token, token::Anchor},
},
},
graph::{Edge, Graph, Node},
};
pub fn populate_graph() -> Graph {
let args = Arguments::default().parse();
let toml_source = match std::fs::read_to_string(args.graph_path) {
Ok(s) => s,
Err(e) => format!("Error: {e}"),
};
let mut graph = deserialize_graph(&Format::TOML, &toml_source);
modulate_graph(&mut graph)
}
fn modulate_graph(in_graph: &mut Graph) -> Graph {
in_graph.map_lowercase_keys();
let nodes = modulate_nodes(in_graph);
let mut graph = Graph {
incoming: make_incoming(&nodes),
nodes,
..in_graph.clone()
};
graph.parse();
graph
}
fn modulate_nodes(graph: &Graph) -> HashMap<String, Node> {
let in_nodes = graph.nodes.clone();
let mut first_pass_nodes: HashMap<String, Node> = HashMap::default();
for (key, node) in in_nodes.clone() {
let connections = node.connections.clone().unwrap_or_default();
let mut new_edges = connections.clone();
// Modulate connections
for (connection_key, edge) in connections {
let mut new_edge = edge.clone();
// Populate empty "from" IDs in edges with node's ID
if edge.from.is_empty() {
new_edge.from.clone_from(&connection_key);
}
// Flag detached edges
if !in_nodes.contains_key(&edge.to) {
new_edge.detached = true;
}
if let Some(e) = new_edges.get_mut(&connection_key) {
*e = new_edge;
}
}
// Create connections for each link
for link in &node.links {
new_edges.insert(
link.clone(),
Edge {
from: key.clone(),
to: link.clone(),
detached: !in_nodes.clone().contains_key(link),
},
);
}
// Populate empty titles with IDs
let new_title = if node.title.is_empty() {
key.clone()
} else {
node.title.clone()
};
// 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())
{
String::from(first)
} else {
node.text.clone()
};
let mut candidate =
if let Some(dot_split) = first_line.split_once('.') {
format!("{}.", dot_split.0)
} else {
first_line
};
if candidate.len() > 300 {
candidate.truncate(300);
candidate.push('…');
}
candidate
} else {
node.summary.clone()
};
// Assemble new node
let new_node = Node {
id: key.clone(),
title: new_title,
summary: flatten(&summary, graph),
connections: Some(new_edges),
..node.clone()
};
first_pass_nodes.insert(key.clone(), new_node);
}
let mut second_pass_nodes: HashMap<String, Node> = HashMap::default();
for (key, node) in first_pass_nodes.clone() {
let first_pass_graph = Graph {
nodes: first_pass_nodes.clone(),
..graph.clone()
};
// Parse node text
let (text, tokens) = content::rich_parse(&node.text, &first_pass_graph);
// Create connections for each anchor
let parsed_anchors =
tokens.iter().filter(|t| matches!(t, Token::Anchor(_)));
let mut anchors: Vec<Anchor> = vec![];
for anchor in parsed_anchors {
if let Token::Anchor(a) = anchor {
anchors.push(*a.clone());
}
}
let mut new_edges = node.connections.clone().unwrap_or_default();
for anchor in anchors {
if let Some(anchor_node) = anchor.node() {
new_edges.insert(
anchor_node.id.clone(),
Edge {
from: key.clone(),
to: anchor_node.id,
detached: false,
},
);
}
}
// Assemble new node
let new_node = Node {
connections: Some(new_edges),
text,
..node.clone()
};
second_pass_nodes.insert(key.clone(), new_node);
}
second_pass_nodes
}
pub enum Format {
TOML,
JSON,
}
pub fn serialize_graph(out_format: &Format, graph: &Graph) -> String {
match *out_format {
Format::TOML => match toml::to_string(graph) {
Ok(s) => s,
Err(e) => e.to_string(),
},
Format::JSON => match serde_json::to_string(graph) {
Ok(s) => s,
Err(e) => e.to_string(),
},
}
}
pub fn deserialize_graph(in_format: &Format, serial: &str) -> Graph {
match *in_format {
Format::TOML => match toml::from_str(serial) {
Ok(g) => g,
Err(error) => Graph::new(Some(&error.to_string())),
},
Format::JSON => match serde_json::from_str(serial) {
Ok(g) => g,
Err(error) => Graph::new(Some(&error.to_string())),
},
}
}
// Construct a HashMap with incoming connections (reversed edges)
fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
let mut incoming: HashMap<String, Vec<Edge>> = HashMap::default();
for node in nodes.clone().into_values() {
let empty_vec: Vec<Edge> = vec![];
for edge in node.connections.clone().unwrap_or_default().values() {
let mut edges =
incoming.get(&edge.to.clone()).unwrap_or(&empty_vec).clone();
edges.extend_from_slice(std::slice::from_ref(edge));
incoming.insert(edge.to.clone(), edges.clone());
}
}
incoming
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn good_json() {
let json = r#"
{
"nodes": {
"JSON": {
"text": "",
"title": "JSON",
"links": [],
"id": "JSON",
"hidden": false,
"connections": {}
}
},
"root_node": "JSON"
}
"#;
let graph = deserialize_graph(&Format::JSON, json);
assert!(graph.meta.messages.is_empty());
}
#[test]
fn bad_json() {
let graph = deserialize_graph(&Format::JSON, ":::");
let message = graph.meta.messages.first().unwrap();
assert!(message.contains("expected value at line 1 column 1"));
}
}
#[cfg(test)]
mod serial_tests {
use super::*;
#[test]
fn bad_graph_path() {
let original_working_directory = std::env::current_dir().unwrap();
assert!(
std::env::set_current_dir(std::path::Path::new(
"tests/mocks/no_graph"
))
.is_ok()
);
let graph = populate_graph();
let message = graph.meta.messages.first().unwrap();
assert!(message.contains("TOML parse error"));
assert!(message.contains("No such file or directory"));
assert!(std::env::set_current_dir(original_working_directory).is_ok());
}
}