Merge serial module into the graph module
This commit is contained in:
parent
bd5d46a5d4
commit
697dcc720d
17 changed files with 421 additions and 332 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue