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
294
src/graph.rs
294
src/graph.rs
|
|
@ -1,8 +1,14 @@
|
|||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::syntax::content;
|
||||
use crate::syntax::{
|
||||
command::Arguments,
|
||||
content::{
|
||||
self,
|
||||
parser::{flatten, Token, token::Anchor},
|
||||
},
|
||||
};
|
||||
use crate::prelude::*;
|
||||
pub use {
|
||||
node::Node,
|
||||
|
|
@ -46,17 +52,225 @@ impl std::fmt::Display for QueryResult {
|
|||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn new(message: Option<&str>) -> Graph {
|
||||
pub fn error(message: Option<&str>) -> Graph {
|
||||
let graph = Graph::default();
|
||||
Graph {
|
||||
nodes: HashMap::default(),
|
||||
root_node: "VoidNode".to_string(),
|
||||
incoming: HashMap::default(),
|
||||
lowercase_keymap: HashMap::default(),
|
||||
meta: Meta {
|
||||
config: Config::default(),
|
||||
version: (0, 1, 0),
|
||||
messages: message.map_or(vec![], |m| vec![m.to_string()]),
|
||||
..graph.meta
|
||||
},
|
||||
..graph
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a TOML file from the default location and returns a modulated Graph
|
||||
pub fn load() -> Graph {
|
||||
Self::load_file("")
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn load_file(path: &str) -> Graph {
|
||||
let mut graph = if path.is_empty() {
|
||||
Self::read_file(None)
|
||||
} else {
|
||||
Self::read_file(Some(path))
|
||||
};
|
||||
graph.modulate();
|
||||
graph
|
||||
}
|
||||
|
||||
/// Reads a TOML fie into a Graph without modulating it
|
||||
pub fn read_file(in_path: Option<&str>) -> 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) {
|
||||
Ok(s) => s,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
};
|
||||
|
||||
Self::from_serial(&toml_source, &Format::TOML)
|
||||
}
|
||||
|
||||
pub fn from_serial(serial: &str, format: &Format) -> Graph {
|
||||
match *format {
|
||||
Format::TOML => match toml::from_str(serial) {
|
||||
Ok(g) => g,
|
||||
Err(error) => Graph::error(Some(&error.to_string())),
|
||||
},
|
||||
Format::JSON => match serde_json::from_str(serial) {
|
||||
Ok(g) => g,
|
||||
Err(error) => Graph::error(Some(&error.to_string())),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_serial(graph: &Graph, format: &Format) -> String {
|
||||
match *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 modulate(&mut self) {
|
||||
self.map_lowercase_keys();
|
||||
self.modulate_nodes();
|
||||
self.modulate_edges();
|
||||
self.map_incoming();
|
||||
self.parse_config();
|
||||
}
|
||||
|
||||
// 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() {
|
||||
let mut edges = self
|
||||
.incoming
|
||||
.get(&edge.to.clone())
|
||||
.unwrap_or(&vec![])
|
||||
.clone();
|
||||
edges.extend_from_slice(std::slice::from_ref(edge));
|
||||
self.incoming.insert(edge.to.clone(), edges.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn modulate_nodes(&mut self) {
|
||||
let in_nodes = self.nodes.clone();
|
||||
|
||||
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 = false;
|
||||
}
|
||||
|
||||
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, self),
|
||||
connections: Some(new_edges),
|
||||
..node.clone()
|
||||
};
|
||||
|
||||
self.nodes.insert(key.clone(), new_node);
|
||||
}
|
||||
}
|
||||
|
||||
fn modulate_edges(&mut self) {
|
||||
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();
|
||||
|
||||
// Create connections for each anchor
|
||||
let mut parsed_anchor_tokens = parse_output
|
||||
.tokens
|
||||
.iter()
|
||||
.filter(|t| matches!(t, Token::Anchor(_)))
|
||||
.cloned()
|
||||
.collect::<Vec<Token>>();
|
||||
parsed_anchor_tokens.extend_from_slice(
|
||||
&parse_output
|
||||
.format_tokens
|
||||
.iter()
|
||||
.filter(|t| matches!(t, Token::Anchor(_)))
|
||||
.cloned()
|
||||
.collect::<Vec<Token>>(),
|
||||
);
|
||||
let mut anchors: Vec<Anchor> = vec![];
|
||||
for token in parsed_anchor_tokens {
|
||||
if let Token::Anchor(token_data) = token {
|
||||
anchors.push(*token_data.clone());
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +338,7 @@ impl Graph {
|
|||
self.nodes.get(&self.root_node).cloned()
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) {
|
||||
pub fn parse_config(&mut self) {
|
||||
self.meta.config.footer_text =
|
||||
content::parse(&self.meta.config.footer_text, self);
|
||||
self.meta.config.about_text =
|
||||
|
|
@ -132,13 +346,18 @@ impl Graph {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum Format {
|
||||
TOML,
|
||||
JSON,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_graph() {
|
||||
let graph = Graph::new(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"));
|
||||
let graph = Graph::error(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"));
|
||||
assert!(graph.nodes.is_empty());
|
||||
assert!(graph.incoming.is_empty());
|
||||
assert_eq!(
|
||||
|
|
@ -146,4 +365,57 @@ mod tests {
|
|||
"ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn good_json() {
|
||||
let json = r#"
|
||||
{
|
||||
"nodes": {
|
||||
"JSON": {
|
||||
"text": "",
|
||||
"title": "JSON",
|
||||
"links": [],
|
||||
"id": "JSON",
|
||||
"hidden": false,
|
||||
"connections": {}
|
||||
}
|
||||
},
|
||||
"root_node": "JSON"
|
||||
}
|
||||
"#;
|
||||
|
||||
let graph = Graph::from_serial(json, &Format::JSON);
|
||||
assert!(graph.meta.messages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_json() {
|
||||
let graph = Graph::from_serial(":::", &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 = Graph::load();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Meta {
|
||||
pub config: Config,
|
||||
#[serde(default = "mkversion")]
|
||||
pub version: (u8, u8, u8),
|
||||
#[serde(default)]
|
||||
pub version: Option<Version>,
|
||||
#[serde(default)]
|
||||
pub messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Meta {
|
||||
fn default() -> Meta {
|
||||
Meta {
|
||||
config: Config::default(),
|
||||
version: Version::from_env(),
|
||||
messages: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
|
|
@ -99,25 +109,22 @@ fn mkfalse() -> bool {
|
|||
fn mk8() -> u16 {
|
||||
8
|
||||
}
|
||||
fn mkversion() -> (u8, u8, u8) {
|
||||
(0, 0, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::graph::Graph;
|
||||
|
||||
use super::*;
|
||||
use crate::syntax::serial::populate_graph;
|
||||
|
||||
#[test]
|
||||
fn empty_footer_text() {
|
||||
let mut graph = populate_graph();
|
||||
let mut graph = Graph::load();
|
||||
|
||||
graph.meta.config = Config {
|
||||
footer_text: String::default(),
|
||||
..graph.meta.config
|
||||
};
|
||||
|
||||
graph.parse();
|
||||
graph.parse_config();
|
||||
|
||||
println!("{:?}", graph.meta.config.footer_text);
|
||||
assert!(graph.meta.config.footer_text.is_empty());
|
||||
|
|
@ -126,14 +133,14 @@ mod tests {
|
|||
#[test]
|
||||
fn config_footer_text() {
|
||||
let payload = "0kqBrdS8NPrU4xVxh2xW0hUzAw926JCQ";
|
||||
let mut graph = populate_graph();
|
||||
let mut graph = Graph::load();
|
||||
|
||||
graph.meta.config = Config {
|
||||
footer_text: format!("`{payload}`"),
|
||||
..graph.meta.config
|
||||
};
|
||||
|
||||
graph.parse();
|
||||
graph.parse_config();
|
||||
|
||||
assert!(
|
||||
graph
|
||||
|
|
@ -149,14 +156,14 @@ mod tests {
|
|||
#[test]
|
||||
fn config_about_text() {
|
||||
let payload = "ZqPFl84JlzSS0QUo61RwTUPONIE78Lmw";
|
||||
let mut graph = populate_graph();
|
||||
let mut graph = Graph::load();
|
||||
|
||||
graph.meta.config = Config {
|
||||
about_text: format!("`{payload}`"),
|
||||
..graph.meta.config
|
||||
};
|
||||
|
||||
graph.parse();
|
||||
graph.parse_config();
|
||||
|
||||
assert!(
|
||||
graph
|
||||
|
|
@ -169,3 +176,59 @@ mod tests {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
|
||||
pub struct Version {
|
||||
major: u8,
|
||||
minor: u8,
|
||||
patch: u8,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn from_env() -> Option<Version> {
|
||||
Self::from(env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
pub fn from(version: &str) -> Option<Version> {
|
||||
let triple: Vec<String> =
|
||||
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.trim_start_matches('v').trim().parse().ok()?
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let minor: u8 = if let Some(s) = triple.get(1) {
|
||||
s.trim().parse().ok()?
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let patch: u8 = if let Some(s) = triple.get(2) {
|
||||
s.trim().parse().ok()?
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let conditions = has_two_dots
|
||||
&& has_three_elements
|
||||
&& !has_whitespace
|
||||
&& !has_contiguous_dots
|
||||
&& !ends_with_dot
|
||||
&& !starts_with_dot;
|
||||
|
||||
conditions.then_some(Version {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{backtrace, io, panic};
|
||||
|
||||
use en::{prelude::*, ONSET, syntax::serial::populate_graph, syntax};
|
||||
use en::{prelude::*, ONSET, graph::Graph, syntax};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
|
|
@ -34,7 +34,7 @@ async fn main() -> io::Result<()> {
|
|||
}
|
||||
}));
|
||||
|
||||
let graph = populate_graph();
|
||||
let graph = Graph::load();
|
||||
let router = en::router::new(&graph);
|
||||
|
||||
let listener =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use axum::{routing::get, Router};
|
||||
|
||||
use crate::{syntax::serial::Format, graph::Graph};
|
||||
use crate::{graph::Format, graph::Graph};
|
||||
|
||||
mod handlers {
|
||||
pub mod graph;
|
||||
|
|
@ -69,8 +69,7 @@ pub fn new(graph: &Graph) -> Router {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
syntax::serial::populate_graph,
|
||||
graph::{Config, Meta},
|
||||
graph::{Graph, Config, Meta},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
|
@ -82,7 +81,7 @@ mod tests {
|
|||
use tower::ServiceExt as _;
|
||||
|
||||
async fn request(uri: &str, config: Option<&Config>) -> Response<Body> {
|
||||
let default_graph = populate_graph();
|
||||
let default_graph = Graph::load();
|
||||
let graph = Graph {
|
||||
meta: Meta {
|
||||
config: config
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use axum::{
|
|||
http::{Response, StatusCode, header},
|
||||
};
|
||||
|
||||
use crate::{syntax::serial::populate_graph, router::handlers};
|
||||
use crate::{graph::Graph, router::handlers};
|
||||
|
||||
pub(in crate::router::handlers) fn by_code(
|
||||
code: Option<u16>,
|
||||
|
|
@ -26,7 +26,7 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
|
|||
|
||||
let out_code = code.unwrap_or(500);
|
||||
let out_message = &message.unwrap_or("Unknown error");
|
||||
let config = populate_graph().meta.config;
|
||||
let config = Graph::load().meta.config;
|
||||
|
||||
context.insert(
|
||||
"title",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use axum::{
|
|||
use crate::prelude::*;
|
||||
use crate::{
|
||||
router::handlers,
|
||||
syntax::serial::{Format, populate_graph, serialize_graph},
|
||||
graph::{Graph, Format},
|
||||
};
|
||||
|
||||
/// # Panics
|
||||
|
|
@ -35,8 +35,8 @@ pub async fn file(file_path: &str, content_type: &str) -> Response<Body> {
|
|||
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn serial(format: &Format) -> Response<Body> {
|
||||
let graph = populate_graph();
|
||||
let body = serialize_graph(format, &graph);
|
||||
let graph = Graph::load();
|
||||
let body = Graph::to_serial(&graph, format);
|
||||
|
||||
match *format {
|
||||
Format::TOML => handlers::raw::make_response(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use axum::response::IntoResponse as _;
|
|||
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
|
||||
|
||||
use crate::graph::Edge;
|
||||
use crate::{syntax::serial::populate_graph, router::handlers, graph::Node};
|
||||
use crate::{graph::Graph, router::handlers, graph::Node};
|
||||
|
||||
pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||
let graph = populate_graph();
|
||||
let graph = Graph::load();
|
||||
let result = graph.find_node(&id);
|
||||
let found = result.node.is_some();
|
||||
let nodes: Vec<Node> = graph.nodes.clone().into_values().collect();
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ use axum::{
|
|||
Form,
|
||||
};
|
||||
|
||||
use crate::{syntax::serial::populate_graph, router::handlers, graph::Node};
|
||||
use crate::{
|
||||
graph::{Graph, Node},
|
||||
router::handlers,
|
||||
};
|
||||
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn page(template: &str) -> Response<Body> {
|
||||
let mut context = tera::Context::default();
|
||||
let graph = populate_graph();
|
||||
let graph = Graph::load();
|
||||
let root_node = graph.get_root().unwrap_or_default();
|
||||
let nodes: Vec<Node> = graph.nodes.into_values().collect();
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ fn emergency_wrap(error: &tera::Error) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::graph::Graph;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ mod tests {
|
|||
let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy";
|
||||
let mut context = tera::Context::default();
|
||||
let node = crate::graph::Node::new(Some(payload.to_string()));
|
||||
let graph = crate::syntax::serial::populate_graph();
|
||||
let graph = Graph::load();
|
||||
context.insert("node", &node);
|
||||
context
|
||||
.insert("text", &crate::syntax::content::parse(&node.text, &graph));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
pub mod command;
|
||||
pub mod content;
|
||||
pub mod serial;
|
||||
|
|
|
|||
|
|
@ -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