Make anchors aware of the nodes they point to
This commit is contained in:
parent
eb96b456ef
commit
3fa399c317
31 changed files with 366 additions and 230 deletions
|
|
@ -24,7 +24,7 @@ alias w := run-watch
|
||||||
|
|
||||||
[private]
|
[private]
|
||||||
quick-assess:
|
quick-assess:
|
||||||
{{ just_cmd }} test lint check
|
{{ just_cmd }} lint check quick-test-cover
|
||||||
|
|
||||||
[private]
|
[private]
|
||||||
quick-assess-run:
|
quick-assess-run:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ pub fn elog(function: &str, message: &str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paths in this slice suppress logging if found in the stack trace
|
// Paths in this slice suppress logging if found in the stack trace
|
||||||
pub const SKIP_PATHS: &[&str] = &["en::types::Config::parse_text"];
|
pub const SKIP_PATHS: &[&str] = &["en::types::Graph::parse"];
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log {
|
macro_rules! log {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::{syntax::serial::populate_graph, router::handlers, types::Node};
|
||||||
pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||||
let graph = populate_graph();
|
let graph = populate_graph();
|
||||||
let result = graph.find_node(&id);
|
let result = graph.find_node(&id);
|
||||||
let nodes: Vec<Node> = graph.nodes.into_values().collect();
|
let nodes: Vec<Node> = graph.nodes.clone().into_values().collect();
|
||||||
let not_found = result.node.is_none();
|
let not_found = result.node.is_none();
|
||||||
let node = result
|
let node = result
|
||||||
.node
|
.node
|
||||||
|
|
@ -29,7 +29,7 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||||
let mut context = tera::Context::default();
|
let mut context = tera::Context::default();
|
||||||
context.insert("node", &node);
|
context.insert("node", &node);
|
||||||
context.insert("nodes", &nodes);
|
context.insert("nodes", &nodes);
|
||||||
context.insert("text", &content::parse(&node.text, &graph.meta.config));
|
context.insert("text", &content::parse(&node.text, &graph));
|
||||||
context.insert("incoming", &graph.incoming.get(&id));
|
context.insert("incoming", &graph.incoming.get(&id));
|
||||||
context.insert("config", &graph.meta.config);
|
context.insert("config", &graph.meta.config);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,10 +156,8 @@ mod tests {
|
||||||
let node = crate::types::Node::new(Some(payload.to_string()));
|
let node = crate::types::Node::new(Some(payload.to_string()));
|
||||||
let graph = crate::syntax::serial::populate_graph();
|
let graph = crate::syntax::serial::populate_graph();
|
||||||
context.insert("node", &node);
|
context.insert("node", &node);
|
||||||
context.insert(
|
context
|
||||||
"text",
|
.insert("text", &crate::syntax::content::parse(&node.text, &graph));
|
||||||
&crate::syntax::content::parse(&node.text, &graph.meta.config),
|
|
||||||
);
|
|
||||||
context.insert("incoming", &graph.incoming.get(&node.id));
|
context.insert("incoming", &graph.incoming.get(&node.id));
|
||||||
context.insert("config", &graph.meta.config);
|
context.insert("config", &graph.meta.config);
|
||||||
let (body, status) = render("node.html", &context, None);
|
let (body, status) = render("node.html", &context, None);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use parser::{token::Token, lexeme::Lexeme};
|
use parser::{token::Token, lexeme::Lexeme};
|
||||||
|
|
||||||
use crate::types::Config;
|
use crate::types::Graph;
|
||||||
|
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|
||||||
|
|
@ -8,12 +8,13 @@ pub trait Parseable: std::fmt::Display {
|
||||||
fn probe(lexeme: &Lexeme) -> bool;
|
fn probe(lexeme: &Lexeme) -> bool;
|
||||||
fn lex(lexeme: &Lexeme) -> Self;
|
fn lex(lexeme: &Lexeme) -> Self;
|
||||||
fn render(&self) -> String;
|
fn render(&self) -> String;
|
||||||
|
fn flatten(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Probe = fn(&Lexeme) -> bool;
|
type Probe = fn(&Lexeme) -> bool;
|
||||||
type Lexer = fn(&Lexeme) -> Token;
|
type Lexer = fn(&Lexeme) -> Token;
|
||||||
type LexMap<'lm> = &'lm [(Probe, Lexer)];
|
type LexMap<'lm> = &'lm [(Probe, Lexer)];
|
||||||
|
|
||||||
pub fn parse(text: &str, config: &Config) -> String {
|
pub fn parse(text: &str, graph: &Graph) -> String {
|
||||||
parser::read(text, config)
|
parser::read(text, graph)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{prelude::*, types::Config};
|
use crate::{prelude::*, types::Graph};
|
||||||
use super::{Parseable as _, Token, LexMap};
|
use super::{Parseable as _, Token, LexMap};
|
||||||
use token::{linebreak::LineBreak, literal::Literal};
|
use token::{linebreak::LineBreak, literal::Literal};
|
||||||
use lexeme::Lexeme;
|
use lexeme::Lexeme;
|
||||||
|
|
@ -20,7 +20,7 @@ const LEXMAP: LexMap = &[
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
fn lex(text: &str, map: LexMap, config: &Config, blocking: bool) -> Vec<Token> {
|
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> Vec<Token> {
|
||||||
let mut tokens: Vec<Token> = Vec::default();
|
let mut tokens: Vec<Token> = Vec::default();
|
||||||
let mut state = state::State::default();
|
let mut state = state::State::default();
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ fn lex(text: &str, map: LexMap, config: &Config, blocking: bool) -> Vec<Token> {
|
||||||
&mut state,
|
&mut state,
|
||||||
&mut tokens,
|
&mut tokens,
|
||||||
&mut iterator,
|
&mut iterator,
|
||||||
config,
|
graph,
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +59,7 @@ fn lex(text: &str, map: LexMap, config: &Config, blocking: bool) -> Vec<Token> {
|
||||||
&mut state,
|
&mut state,
|
||||||
&mut tokens,
|
&mut tokens,
|
||||||
&mut iterator,
|
&mut iterator,
|
||||||
|
graph,
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -77,20 +78,28 @@ fn lex(text: &str, map: LexMap, config: &Config, blocking: bool) -> Vec<Token> {
|
||||||
tokens
|
tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn read(input: &str, graph: &Graph) -> String {
|
||||||
|
parse(&lex(input, LEXMAP, graph, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply end-to-end point and inline parsing for nested formatting, such as
|
||||||
|
/// inside the display text of anchors and list items
|
||||||
|
pub fn nest(input: &str, graph: &Graph) -> String {
|
||||||
|
parse(&lex(input, LEXMAP, graph, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 flat = tokens.iter().map(Token::flatten).collect::<String>();
|
||||||
|
log!("Flattened {tokens:?} to {flat}");
|
||||||
|
flat
|
||||||
|
}
|
||||||
|
|
||||||
fn parse(tokens: &[Token]) -> String {
|
fn parse(tokens: &[Token]) -> String {
|
||||||
tokens.iter().map(Token::render).collect::<String>()
|
tokens.iter().map(Token::render).collect::<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply end-to-end point and inline parsing for nested contexts, such as
|
|
||||||
/// inside the displayed text of other tokens like anchors and list items
|
|
||||||
pub fn nest(input: &str, config: &Config) -> String {
|
|
||||||
parse(&lex(input, LEXMAP, config, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn read(input: &str, config: &Config) -> String {
|
|
||||||
parse(&lex(input, LEXMAP, config, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -101,7 +110,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn read_noconfig(input: &str) -> String {
|
fn read_noconfig(input: &str) -> String {
|
||||||
read(input, &Graph::new(None).meta.config)
|
read(input, &Graph::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -112,7 +121,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_sample() {
|
fn mixed_sample() {
|
||||||
let en = "`this |test|` tries ## to |brea|k|: things";
|
let en = "`this |test|` tries ## to |brea|k|: things";
|
||||||
let html = r#"<p><code>this |test|</code> tries ## to <a href="/node/k">brea</a>: things</p>"#;
|
let html = r#"<p><code>this |test|</code> tries ## to <a class="detached" title="" href="/node/k">brea</a>: things</p>"#;
|
||||||
|
|
||||||
assert_eq!(read_noconfig(en), html);
|
assert_eq!(read_noconfig(en), html);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
syntax::content::parser::{
|
syntax::content::parser::{
|
||||||
state::State, context::Inline, lexeme::Lexeme, token::Token,
|
context::Inline, lexeme::Lexeme, state::State, token::Token,
|
||||||
},
|
},
|
||||||
|
types::Graph,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Handles open anchor contexts until an anchor token is fully parsed.
|
/// Handles open anchor contexts until an anchor token is fully parsed.
|
||||||
|
|
@ -16,6 +17,7 @@ pub fn parse(
|
||||||
lexeme: &Lexeme,
|
lexeme: &Lexeme,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
tokens: &mut Vec<Token>,
|
tokens: &mut Vec<Token>,
|
||||||
|
graph: &Graph,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
log!("Solving: {}", state.clone().buffers.anchor);
|
log!("Solving: {}", state.clone().buffers.anchor);
|
||||||
let buffer = &mut state.buffers.anchor;
|
let buffer = &mut state.buffers.anchor;
|
||||||
|
|
@ -61,19 +63,16 @@ pub fn parse(
|
||||||
candidate.set_destination(Some(&candidate.text().clone()));
|
candidate.set_destination(Some(&candidate.text().clone()));
|
||||||
candidate.text_push("s");
|
candidate.text_push("s");
|
||||||
if lexeme.last() {
|
if lexeme.last() {
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
push(None, tokens, state, graph);
|
||||||
state.context.inline = Inline::None;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if lexeme.match_char('|') && lexeme.is_next_delimiter() {
|
} else if lexeme.match_char('|') && lexeme.is_next_delimiter() {
|
||||||
log!("End: Pipe followed by delimiter");
|
log!("End: Pipe followed by delimiter");
|
||||||
if buffer.destination.is_empty() {
|
if buffer.destination.is_empty() {
|
||||||
candidate.set_destination(Some(&candidate.text().clone()));
|
push(Some(&candidate.text().clone()), tokens, state, graph);
|
||||||
} else {
|
} else {
|
||||||
candidate.set_destination(Some(&buffer.destination.clone()));
|
push(Some(&buffer.destination.clone()), tokens, state, graph);
|
||||||
}
|
}
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
|
||||||
state.context.inline = Inline::None;
|
|
||||||
return true;
|
return true;
|
||||||
} else if lexeme.match_char('|') && !candidate.balanced() {
|
} else if lexeme.match_char('|') && !candidate.balanced() {
|
||||||
log!("State: Found a pipe, but no boundary: destination follows");
|
log!("State: Found a pipe, but no boundary: destination follows");
|
||||||
|
|
@ -90,23 +89,17 @@ pub fn parse(
|
||||||
return true;
|
return true;
|
||||||
} else if !candidate.external() && lexeme.is_delimiter() {
|
} else if !candidate.external() && lexeme.is_delimiter() {
|
||||||
log!("End: Internal anchor trailed by delimiter");
|
log!("End: Internal anchor trailed by delimiter");
|
||||||
candidate.set_destination(Some(&buffer.destination.clone()));
|
push(Some(&buffer.destination.clone()), tokens, state, graph);
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
|
||||||
state.context.inline = Inline::None;
|
|
||||||
return false;
|
return false;
|
||||||
} else if lexeme.is_next_whitespace() {
|
} else if lexeme.is_next_whitespace() {
|
||||||
log!("End: next is whitespace");
|
log!("End: next is whitespace");
|
||||||
buffer.destination.push_str(&lexeme.text());
|
buffer.destination.push_str(&lexeme.text());
|
||||||
candidate.set_destination(Some(&buffer.destination.clone()));
|
push(Some(&buffer.destination.clone()), tokens, state, graph);
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
|
||||||
state.context.inline = Inline::None;
|
|
||||||
return true;
|
return true;
|
||||||
} else if lexeme.last() {
|
} else if lexeme.last() {
|
||||||
log!("End: end of input");
|
log!("End: end of input");
|
||||||
buffer.destination.push_str(&lexeme.text());
|
buffer.destination.push_str(&lexeme.text());
|
||||||
candidate.set_destination(Some(&buffer.destination.clone()));
|
push(Some(&buffer.destination.clone()), tokens, state, graph);
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
|
||||||
state.context.inline = Inline::None;
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// This else branch is the 'no end found yet' state and will keep
|
// This else branch is the 'no end found yet' state and will keep
|
||||||
|
|
@ -119,9 +112,7 @@ pub fn parse(
|
||||||
);
|
);
|
||||||
buffer.destination.push_str(&lexeme.text());
|
buffer.destination.push_str(&lexeme.text());
|
||||||
if lexeme.last() {
|
if lexeme.last() {
|
||||||
candidate.set_destination(Some(&buffer.destination.clone()));
|
push(Some(&buffer.destination.clone()), tokens, state, graph);
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
|
||||||
state.context.inline = Inline::None;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -136,47 +127,79 @@ pub fn parse(
|
||||||
"Anchor context parsing done but no destination found: {:#?}",
|
"Anchor context parsing done but no destination found: {:#?}",
|
||||||
state.buffers.anchor
|
state.buffers.anchor
|
||||||
);
|
);
|
||||||
tokens.push(Token::Anchor(candidate.clone()));
|
push(None, tokens, state, graph);
|
||||||
state.context.inline = Inline::None;
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push(
|
||||||
|
d: Option<&str>,
|
||||||
|
tokens: &mut Vec<Token>,
|
||||||
|
state: &mut State,
|
||||||
|
graph: &Graph,
|
||||||
|
) {
|
||||||
|
let candidate = &mut state.buffers.anchor.candidate;
|
||||||
|
if d.is_some() {
|
||||||
|
candidate.set_destination(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(node_id) = candidate.node_id()
|
||||||
|
&& let Some(node) = graph.find_node(&node_id).node
|
||||||
|
{
|
||||||
|
log!("Found matching node {node:?} for anchor candidate {candidate}");
|
||||||
|
candidate.set_node(&node);
|
||||||
|
} else {
|
||||||
|
log!("Could not find a matching node for anchor candidate {candidate}");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push(Token::Anchor(Box::new(candidate.clone())));
|
||||||
|
state.context.inline = Inline::None;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{syntax::content::parser, types::Graph};
|
use crate::{syntax::content::parser, types::Graph};
|
||||||
|
|
||||||
fn read(input: &str) -> String {
|
fn read(input: &str) -> String {
|
||||||
parser::read(input, &Graph::new(None).meta.config)
|
parser::read(input, &Graph::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flanking() {
|
fn flanking() {
|
||||||
assert_eq!(read("|Node|"), r#"<p><a href="/node/Node">Node</a></p>"#);
|
assert_eq!(
|
||||||
|
read("|Node|"),
|
||||||
|
r#"<p><a class="detached" title="" href="/node/Node">Node</a></p>"#
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flanking_with_trailing_comma() {
|
fn flanking_with_trailing_comma() {
|
||||||
assert_eq!(read("|Node|,"), r#"<p><a href="/node/Node">Node</a>,</p>"#);
|
assert_eq!(
|
||||||
|
read("|Node|,"),
|
||||||
|
r#"<p><a class="detached" title="" href="/node/Node">Node</a>,</p>"#
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flanking_with_trailing_comma_and_space() {
|
fn flanking_with_trailing_comma_and_space() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|Node|, at"),
|
read("|Node|, at"),
|
||||||
r#"<p><a href="/node/Node">Node</a>, at</p>"#
|
r#"<p><a class="detached" title="" href="/node/Node">Node</a>, at</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flanking_at_eoi() {
|
fn flanking_at_eoi() {
|
||||||
assert_eq!(read("|Node|"), r#"<p><a href="/node/Node">Node</a></p>"#);
|
assert_eq!(
|
||||||
|
read("|Node|"),
|
||||||
|
r#"<p><a class="detached" title="" href="/node/Node">Node</a></p>"#
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn needless_three_pipe_anchor() {
|
fn needless_three_pipe_anchor() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|Node|Destination|"),
|
read("|Node|Destination|"),
|
||||||
r#"<p><a href="/node/Destination">Node</a></p>"#
|
r#"<p><a class="detached" title="" href="/node/Destination">Node</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,7 +207,7 @@ mod tests {
|
||||||
fn nonleading_second_pipe() {
|
fn nonleading_second_pipe() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("Go to Node|Destination|, here"),
|
read("Go to Node|Destination|, here"),
|
||||||
r#"<p>Go to <a href="/node/Destination">Node</a>, here</p>"#,
|
r#"<p>Go to <a class="detached" title="" href="/node/Destination">Node</a>, here</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +215,7 @@ mod tests {
|
||||||
fn anchor_to_node_s() {
|
fn anchor_to_node_s() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("The |letter s|s|'s node: |s|!"),
|
read("The |letter s|s|'s node: |s|!"),
|
||||||
r#"<p>The <a href="/node/s">letter s</a>'s node: <a href="/node/s">s</a>!</p>"#
|
r#"<p>The <a class="detached" title="" href="/node/s">letter s</a>'s node: <a class="detached" title="" href="/node/s">s</a>!</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +223,7 @@ mod tests {
|
||||||
fn nonleading_plural_anchor() {
|
fn nonleading_plural_anchor() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("The flower|s bloomed"),
|
read("The flower|s bloomed"),
|
||||||
r#"<p>The <a href="/node/flower">flowers</a> bloomed</p>"#
|
r#"<p>The <a class="detached" title="" href="/node/flower">flowers</a> bloomed</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +231,7 @@ mod tests {
|
||||||
fn leading_plural_anchor() {
|
fn leading_plural_anchor() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("Interfaces are |element|s of |system|s."),
|
read("Interfaces are |element|s of |system|s."),
|
||||||
r#"<p>Interfaces are <a href="/node/element">elements</a> of <a href="/node/system">systems</a>.</p>"#
|
r#"<p>Interfaces are <a class="detached" title="" href="/node/element">elements</a> of <a class="detached" title="" href="/node/system">systems</a>.</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,7 +239,7 @@ mod tests {
|
||||||
fn leading_multiword_anchor() {
|
fn leading_multiword_anchor() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("interactions are |basic elements| of systems"),
|
read("interactions are |basic elements| of systems"),
|
||||||
r#"<p>interactions are <a href="/node/basic elements">basic elements</a> of systems</p>"#
|
r#"<p>interactions are <a class="detached" title="" href="/node/basic elements">basic elements</a> of systems</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,7 +247,7 @@ mod tests {
|
||||||
fn explicit_end_of_destination() {
|
fn explicit_end_of_destination() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("interactions are |basic elements|BasicElements| of systems"),
|
read("interactions are |basic elements|BasicElements| of systems"),
|
||||||
r#"<p>interactions are <a href="/node/BasicElements">basic elements</a> of systems</p>"#
|
r#"<p>interactions are <a class="detached" title="" href="/node/BasicElements">basic elements</a> of systems</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,20 +255,23 @@ mod tests {
|
||||||
fn explicit_end_of_external_destination() {
|
fn explicit_end_of_external_destination() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("this |anchor example|https://example.com| is external"),
|
read("this |anchor example|https://example.com| is external"),
|
||||||
r#"<p>this <a href="https://example.com">anchor example</a> is external</p>"#
|
r#"<p>this <a class="external" title="" href="https://example.com">anchor example</a> is external</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn anchor_destination_at_eoi() {
|
fn anchor_destination_at_eoi() {
|
||||||
assert_eq!(read("a |b c|d"), r#"<p>a <a href="/node/d">b c</a></p>"#);
|
assert_eq!(
|
||||||
|
read("a |b c|d"),
|
||||||
|
r#"<p>a <a class="detached" title="" href="/node/d">b c</a></p>"#
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn external_anchor_destination_at_eoi() {
|
fn external_anchor_destination_at_eoi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("a b|https://example.com"),
|
read("a b|https://example.com"),
|
||||||
r#"<p>a <a href="https://example.com">b</a></p>"#
|
r#"<p>a <a class="external" title="" href="https://example.com">b</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +279,7 @@ mod tests {
|
||||||
fn nonleading_plural_anchor_at_eoi() {
|
fn nonleading_plural_anchor_at_eoi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("element|s"),
|
read("element|s"),
|
||||||
r#"<p><a href="/node/element">elements</a></p>"#
|
r#"<p><a class="detached" title="" href="/node/element">elements</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +287,7 @@ mod tests {
|
||||||
fn leading_plural_anchor_at_eoi() {
|
fn leading_plural_anchor_at_eoi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|element|s"),
|
read("|element|s"),
|
||||||
r#"<p><a href="/node/element">elements</a></p>"#
|
r#"<p><a class="detached" title="" href="/node/element">elements</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,7 +297,7 @@ mod tests {
|
||||||
read(
|
read(
|
||||||
"a |false dichotomy|https://en.wikipedia.org/wiki/False_dilemma|."
|
"a |false dichotomy|https://en.wikipedia.org/wiki/False_dilemma|."
|
||||||
),
|
),
|
||||||
r#"<p>a <a href="https://en.wikipedia.org/wiki/False_dilemma">false dichotomy</a>.</p>"#
|
r#"<p>a <a class="external" title="" href="https://en.wikipedia.org/wiki/False_dilemma">false dichotomy</a>.</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,7 +310,7 @@ mod tests {
|
||||||
"at rustup.rs",
|
"at rustup.rs",
|
||||||
)),
|
)),
|
||||||
concat!(
|
concat!(
|
||||||
r#"<p><a href="https://rustup.rs/">Rust toolchain</a>"#,
|
r#"<p><a class="external" title="" href="https://rustup.rs/">Rust toolchain</a>"#,
|
||||||
"\n",
|
"\n",
|
||||||
"at rustup.rs</p>",
|
"at rustup.rs</p>",
|
||||||
)
|
)
|
||||||
|
|
@ -295,7 +321,7 @@ mod tests {
|
||||||
fn http_external_anchor_leading_no_third_then_space() {
|
fn http_external_anchor_leading_no_third_then_space() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|Rust toolchain|https://rustup.rs/ at rustup.rs"),
|
read("|Rust toolchain|https://rustup.rs/ at rustup.rs"),
|
||||||
r#"<p><a href="https://rustup.rs/">Rust toolchain</a> at rustup.rs</p>"#
|
r#"<p><a class="external" title="" href="https://rustup.rs/">Rust toolchain</a> at rustup.rs</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,7 +329,7 @@ mod tests {
|
||||||
fn http_external_anchor_leading_no_third_then_eoi() {
|
fn http_external_anchor_leading_no_third_then_eoi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|Rust toolchain|https://rustup.rs/"),
|
read("|Rust toolchain|https://rustup.rs/"),
|
||||||
r#"<p><a href="https://rustup.rs/">Rust toolchain</a></p>"#
|
r#"<p><a class="external" title="" href="https://rustup.rs/">Rust toolchain</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,7 +339,7 @@ mod tests {
|
||||||
read("\n|SomeAnchor|\n"),
|
read("\n|SomeAnchor|\n"),
|
||||||
concat!(
|
concat!(
|
||||||
"\n",
|
"\n",
|
||||||
r#"<p><a href="/node/SomeAnchor">SomeAnchor</a></p>"#
|
r#"<p><a class="detached" title="" href="/node/SomeAnchor">SomeAnchor</a></p>"#
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -323,9 +349,9 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|SomeAnchor|\n|SomeOtherAnchor|\n"),
|
read("|SomeAnchor|\n|SomeOtherAnchor|\n"),
|
||||||
concat!(
|
concat!(
|
||||||
r#"<p><a href="/node/SomeAnchor">SomeAnchor</a>"#,
|
r#"<p><a class="detached" title="" href="/node/SomeAnchor">SomeAnchor</a>"#,
|
||||||
"\n",
|
"\n",
|
||||||
r#"<a href="/node/SomeOtherAnchor">SomeOtherAnchor</a></p>"#
|
r#"<a class="detached" title="" href="/node/SomeOtherAnchor">SomeOtherAnchor</a></p>"#
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -335,10 +361,10 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("|SomeAnchor|\n\n|SomeOtherAnchor|\n"),
|
read("|SomeAnchor|\n\n|SomeOtherAnchor|\n"),
|
||||||
concat!(
|
concat!(
|
||||||
r#"<p><a href="/node/SomeAnchor">SomeAnchor</a></p>"#,
|
r#"<p><a class="detached" title="" href="/node/SomeAnchor">SomeAnchor</a></p>"#,
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
r#"<p><a href="/node/SomeOtherAnchor">SomeOtherAnchor</a></p>"#
|
r#"<p><a class="detached" title="" href="/node/SomeOtherAnchor">SomeOtherAnchor</a></p>"#
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +373,7 @@ mod tests {
|
||||||
fn trailing_anchor() {
|
fn trailing_anchor() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("see acks|acks"),
|
read("see acks|acks"),
|
||||||
r#"<p>see <a href="/node/acks">acks</a></p>"#
|
r#"<p>see <a class="detached" title="" href="/node/acks">acks</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,7 +381,10 @@ mod tests {
|
||||||
fn trailing_anchor_with_newline() {
|
fn trailing_anchor_with_newline() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("\nsee acks|acks\n"),
|
read("\nsee acks|acks\n"),
|
||||||
concat!("\n", r#"<p>see <a href="/node/acks">acks</a></p>"#)
|
concat!(
|
||||||
|
"\n",
|
||||||
|
r#"<p>see <a class="detached" title="" href="/node/acks">acks</a></p>"#
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,7 +412,7 @@ mod tests {
|
||||||
fn anchor_with_trailing_single_quote() {
|
fn anchor_with_trailing_single_quote() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("the |lion|'s mouth"),
|
read("the |lion|'s mouth"),
|
||||||
r#"<p>the <a href="/node/lion">lion</a>'s mouth</p>"#,
|
r#"<p>the <a class="detached" title="" href="/node/lion">lion</a>'s mouth</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,7 +420,7 @@ mod tests {
|
||||||
fn anchor_with_trailing_double_quote() {
|
fn anchor_with_trailing_double_quote() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read(r#"the "|real|" motive"#),
|
read(r#"the "|real|" motive"#),
|
||||||
r#"<p>the "<a href="/node/real">real</a>" motive</p>"#,
|
r#"<p>the "<a class="detached" title="" href="/node/real">real</a>" motive</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,7 +428,7 @@ mod tests {
|
||||||
fn anchor_with_trailing_parenthesis() {
|
fn anchor_with_trailing_parenthesis() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("this (though |true|) was questioned"),
|
read("this (though |true|) was questioned"),
|
||||||
r#"<p>this (though <a href="/node/true">true</a>) was questioned</p>"#,
|
r#"<p>this (though <a class="detached" title="" href="/node/true">true</a>) was questioned</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,7 +436,7 @@ mod tests {
|
||||||
fn anchor_with_leading_single_quote() {
|
fn anchor_with_leading_single_quote() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("the 'real|Reality' motive"),
|
read("the 'real|Reality' motive"),
|
||||||
r#"<p>the '<a href="/node/Reality">real</a>' motive</p>"#,
|
r#"<p>the '<a class="detached" title="" href="/node/Reality">real</a>' motive</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,7 +444,7 @@ mod tests {
|
||||||
fn anchor_with_leading_double_quote() {
|
fn anchor_with_leading_double_quote() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read(r#"the "real|Reality" motive"#),
|
read(r#"the "real|Reality" motive"#),
|
||||||
r#"<p>the "<a href="/node/Reality">real</a>" motive</p>"#,
|
r#"<p>the "<a class="detached" title="" href="/node/Reality">real</a>" motive</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,7 +452,7 @@ mod tests {
|
||||||
fn anchor_with_leading_parenthesis() {
|
fn anchor_with_leading_parenthesis() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("her (last|Surname) name"),
|
read("her (last|Surname) name"),
|
||||||
r#"<p>her (<a href="/node/Surname">last</a>) name</p>"#,
|
r#"<p>her (<a class="detached" title="" href="/node/Surname">last</a>) name</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,7 +460,7 @@ mod tests {
|
||||||
fn anchor_with_internal_apostrophe() {
|
fn anchor_with_internal_apostrophe() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("the |lion's mouth|album was released"),
|
read("the |lion's mouth|album was released"),
|
||||||
r#"<p>the <a href="/node/album">lion's mouth</a> was released</p>"#
|
r#"<p>the <a class="detached" title="" href="/node/album">lion's mouth</a> was released</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,7 +468,7 @@ mod tests {
|
||||||
fn nonleading_anchor_with_internal_apostrophe() {
|
fn nonleading_anchor_with_internal_apostrophe() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("they decided to stay at Jane's|YellowHouse that night"),
|
read("they decided to stay at Jane's|YellowHouse that night"),
|
||||||
r#"<p>they decided to stay at <a href="/node/YellowHouse">Jane's</a> that night</p>"#
|
r#"<p>they decided to stay at <a class="detached" title="" href="/node/YellowHouse">Jane's</a> that night</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,7 +476,7 @@ mod tests {
|
||||||
fn nonleading_anchor_with_internal_apostrophe_at_eoi() {
|
fn nonleading_anchor_with_internal_apostrophe_at_eoi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("they decided to stay at Jane's|YellowHouse"),
|
read("they decided to stay at Jane's|YellowHouse"),
|
||||||
r#"<p>they decided to stay at <a href="/node/YellowHouse">Jane's</a></p>"#
|
r#"<p>they decided to stay at <a class="detached" title="" href="/node/YellowHouse">Jane's</a></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,7 +484,7 @@ mod tests {
|
||||||
fn nonleading_anchor_with_internal_apostrophe_at_soi() {
|
fn nonleading_anchor_with_internal_apostrophe_at_soi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("Jane's|YellowHouse that night"),
|
read("Jane's|YellowHouse that night"),
|
||||||
r#"<p><a href="/node/YellowHouse">Jane's</a> that night</p>"#
|
r#"<p><a class="detached" title="" href="/node/YellowHouse">Jane's</a> that night</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,7 +492,7 @@ mod tests {
|
||||||
fn anchor_with_internal_double_quotes() {
|
fn anchor_with_internal_double_quotes() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read(r#"the |"real"|Truth motive"#),
|
read(r#"the |"real"|Truth motive"#),
|
||||||
r#"<p>the <a href="/node/Truth">"real"</a> motive</p>"#,
|
r#"<p>the <a class="detached" title="" href="/node/Truth">"real"</a> motive</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,7 +500,7 @@ mod tests {
|
||||||
fn anchor_with_internal_double_quotes_wrapping_spaced_words() {
|
fn anchor_with_internal_double_quotes_wrapping_spaced_words() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read(r#"the |"bare reality"|Ideology they believed"#),
|
read(r#"the |"bare reality"|Ideology they believed"#),
|
||||||
r#"<p>the <a href="/node/Ideology">"bare reality"</a> they believed</p>"#,
|
r#"<p>the <a class="detached" title="" href="/node/Ideology">"bare reality"</a> they believed</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,7 +508,7 @@ mod tests {
|
||||||
fn anchor_with_internal_parenthesis() {
|
fn anchor_with_internal_parenthesis() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("her |last (name)|Surname was Amad"),
|
read("her |last (name)|Surname was Amad"),
|
||||||
r#"<p>her <a href="/node/Surname">last (name)</a> was Amad</p>"#,
|
r#"<p>her <a class="detached" title="" href="/node/Surname">last (name)</a> was Amad</p>"#,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,7 +516,7 @@ mod tests {
|
||||||
fn anchor_with_internal_parenthesis_wrapping_spaced_words() {
|
fn anchor_with_internal_parenthesis_wrapping_spaced_words() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("this |truth (though questionable) was fine|Absurd to them "),
|
read("this |truth (though questionable) was fine|Absurd to them "),
|
||||||
r#"<p>this <a href="/node/Absurd">truth (though questionable) was fine</a> to them</p>"#
|
r#"<p>this <a class="detached" title="" href="/node/Absurd">truth (though questionable) was fine</a> to them</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
types::Config,
|
types::Graph,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn parse(
|
pub fn parse(
|
||||||
|
|
@ -22,7 +22,7 @@ pub fn parse(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
tokens: &mut Vec<Token>,
|
tokens: &mut Vec<Token>,
|
||||||
iterator: &mut Peekable<Iter<'_, Lexeme>>,
|
iterator: &mut Peekable<Iter<'_, Lexeme>>,
|
||||||
config: &Config,
|
graph: &Graph,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match state.context.block {
|
match state.context.block {
|
||||||
Block::None => {
|
Block::None => {
|
||||||
|
|
@ -34,7 +34,7 @@ pub fn parse(
|
||||||
} else if Header::probe(lexeme) {
|
} else if Header::probe(lexeme) {
|
||||||
let mut header = Header::lex(lexeme);
|
let mut header = Header::lex(lexeme);
|
||||||
header.dom_id = Some(Header::make_id(
|
header.dom_id = Some(Header::make_id(
|
||||||
config,
|
&graph.meta.config,
|
||||||
iterator.peek().map_or(&Lexeme::default(), |l| l),
|
iterator.peek().map_or(&Lexeme::default(), |l| l),
|
||||||
&mut state.dom_ids,
|
&mut state.dom_ids,
|
||||||
));
|
));
|
||||||
|
|
@ -47,7 +47,7 @@ pub fn parse(
|
||||||
state.context.block = Block::List;
|
state.context.block = Block::List;
|
||||||
state.buffers.list.candidate.ordered = lexeme.match_char('+');
|
state.buffers.list.candidate.ordered = lexeme.match_char('+');
|
||||||
return super::list::parse(
|
return super::list::parse(
|
||||||
lexeme, state, tokens, iterator, config,
|
lexeme, state, tokens, iterator, graph,
|
||||||
);
|
);
|
||||||
} else if Paragraph::probe(lexeme) {
|
} else if Paragraph::probe(lexeme) {
|
||||||
log!("Block Context: None -> Paragraph on {lexeme}");
|
log!("Block Context: None -> Paragraph on {lexeme}");
|
||||||
|
|
@ -80,7 +80,7 @@ pub fn parse(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Block::List => {
|
Block::List => {
|
||||||
return super::list::parse(lexeme, state, tokens, iterator, config);
|
return super::list::parse(lexeme, state, tokens, iterator, graph);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|
@ -103,7 +103,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
fn read(input: &str) -> String {
|
fn read(input: &str) -> String {
|
||||||
parser::read(input, &Graph::new(None).meta.config)
|
parser::read(input, &Graph::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use crate::{
|
||||||
token::{Token, anchor::Anchor, code::Code, literal::Literal},
|
token::{Token, anchor::Anchor, code::Code, literal::Literal},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
types::Graph,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn parse(
|
pub fn parse(
|
||||||
|
|
@ -18,6 +19,7 @@ pub fn parse(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
tokens: &mut Vec<Token>,
|
tokens: &mut Vec<Token>,
|
||||||
iterator: &mut Peekable<Iter<'_, Lexeme>>,
|
iterator: &mut Peekable<Iter<'_, Lexeme>>,
|
||||||
|
graph: &Graph,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match state.context.inline {
|
match state.context.inline {
|
||||||
Inline::None => {
|
Inline::None => {
|
||||||
|
|
@ -54,7 +56,7 @@ pub fn parse(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Inline::Anchor => {
|
Inline::Anchor => {
|
||||||
if context::anchor::parse(lexeme, state, tokens) {
|
if context::anchor::parse(lexeme, state, tokens, graph) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
state::{ListBuffer, State},
|
state::{ListBuffer, State},
|
||||||
token::{Token, item::Item},
|
token::{Token, item::Item},
|
||||||
},
|
},
|
||||||
types::Config,
|
types::Graph,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Handles open list contexts until a list is fully parsed.
|
/// Handles open list contexts until a list is fully parsed.
|
||||||
|
|
@ -25,7 +25,7 @@ pub fn parse(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
tokens: &mut Vec<Token>,
|
tokens: &mut Vec<Token>,
|
||||||
iterator: &mut Peekable<Iter<'_, Lexeme>>,
|
iterator: &mut Peekable<Iter<'_, Lexeme>>,
|
||||||
config: &Config,
|
graph: &Graph,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let buffer = &mut state.buffers.list;
|
let buffer = &mut state.buffers.list;
|
||||||
let candidate = &mut buffer.candidate;
|
let candidate = &mut buffer.candidate;
|
||||||
|
|
@ -52,7 +52,7 @@ pub fn parse(
|
||||||
}
|
}
|
||||||
if item_candidate.depth.is_some() {
|
if item_candidate.depth.is_some() {
|
||||||
// if the current item candidate has a known depth, push it
|
// if the current item candidate has a known depth, push it
|
||||||
item_candidate.text = nest(&item_candidate.text, config);
|
item_candidate.text = nest(&item_candidate.text, graph);
|
||||||
candidate.items.push(item_candidate.clone());
|
candidate.items.push(item_candidate.clone());
|
||||||
}
|
}
|
||||||
// push list candidate, reset state and exit context
|
// push list candidate, reset state and exit context
|
||||||
|
|
@ -64,7 +64,7 @@ pub fn parse(
|
||||||
} else if lexeme.match_char('\n') {
|
} else if lexeme.match_char('\n') {
|
||||||
// found end of item, push it and reset state
|
// found end of item, push it and reset state
|
||||||
log!("Accepting item candidate {item_candidate}");
|
log!("Accepting item candidate {item_candidate}");
|
||||||
item_candidate.text = nest(&item_candidate.text, config);
|
item_candidate.text = nest(&item_candidate.text, graph);
|
||||||
candidate.items.push(item_candidate.clone());
|
candidate.items.push(item_candidate.clone());
|
||||||
*item_candidate = Item::default();
|
*item_candidate = Item::default();
|
||||||
buffer.depth = 0;
|
buffer.depth = 0;
|
||||||
|
|
@ -89,11 +89,11 @@ mod tests {
|
||||||
syntax::content::parser::{
|
syntax::content::parser::{
|
||||||
self, context::list::parse, lexeme::Lexeme, state::State,
|
self, context::list::parse, lexeme::Lexeme, state::State,
|
||||||
},
|
},
|
||||||
types::{Config, Graph},
|
types::Graph,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn read(input: &str) -> String {
|
fn read(input: &str) -> String {
|
||||||
parser::read(input, &Graph::new(None).meta.config)
|
parser::read(input, &Graph::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -273,13 +273,13 @@ mod tests {
|
||||||
fn bad_context() {
|
fn bad_context() {
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
let lexemes = Lexeme::collect(&["a", "b", "c"].map(str::to_string));
|
let lexemes = Lexeme::collect(&["a", "b", "c"].map(str::to_string));
|
||||||
let config = Config::default();
|
let graph = Graph::default();
|
||||||
parse(
|
parse(
|
||||||
&Lexeme::default(),
|
&Lexeme::default(),
|
||||||
&mut state,
|
&mut state,
|
||||||
&mut vec![],
|
&mut vec![],
|
||||||
&mut lexemes.iter().peekable(),
|
&mut lexemes.iter().peekable(),
|
||||||
&config,
|
&graph,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,14 @@ mod tests {
|
||||||
use crate::{syntax::content::parser, types::Graph};
|
use crate::{syntax::content::parser, types::Graph};
|
||||||
|
|
||||||
fn read(input: &str) -> String {
|
fn read(input: &str) -> String {
|
||||||
parser::read(input, &Graph::new(None).meta.config)
|
parser::read(input, &Graph::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oblique_anchor() {
|
fn oblique_anchor() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("w _|S|_ w"),
|
read("w _|S|_ w"),
|
||||||
r#"<p>w <em><a href="/node/S">S</a></em> w</p>"#
|
r#"<p>w <em><a class="detached" title="" href="/node/S">S</a></em> w</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ mod tests {
|
||||||
fn oblique_anchor_with_trailing_comma() {
|
fn oblique_anchor_with_trailing_comma() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("w _|S|_, w"),
|
read("w _|S|_, w"),
|
||||||
r#"<p>w <em><a href="/node/S">S</a></em>, w</p>"#
|
r#"<p>w <em><a class="detached" title="" href="/node/S">S</a></em>, w</p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,9 +88,9 @@ mod tests {
|
||||||
fn oblique() {
|
fn oblique() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read(
|
read(
|
||||||
"_|this anchor is oblique|o as are these literals_ but not these _just these_, not this _and these with an |anchor| again_"
|
"_|this anchor is oblique|o as are these literals_ but not these _just these_, not this _and these with an |anc80r| again_"
|
||||||
),
|
),
|
||||||
r#"<p><em><a href="/node/o">this anchor is oblique</a> as are these literals</em> but not these <em>just these</em>, not this <em>and these with an <a href="/node/anchor">anchor</a> again</em></p>"#
|
r#"<p><em><a class="detached" title="" href="/node/o">this anchor is oblique</a> as are these literals</em> but not these <em>just these</em>, not this <em>and these with an <a class="detached" title="" href="/node/anc80r">anc80r</a> again</em></p>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ pub mod underline;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum Token {
|
pub enum Token {
|
||||||
Anchor(anchor::Anchor),
|
Anchor(Box<anchor::Anchor>),
|
||||||
Bold(bold::Bold),
|
Bold(bold::Bold),
|
||||||
CheckBox(checkbox::CheckBox),
|
CheckBox(checkbox::CheckBox),
|
||||||
Code(code::Code),
|
Code(code::Code),
|
||||||
|
|
@ -53,6 +53,25 @@ impl Token {
|
||||||
Token::Underline(ref d) => d.render(),
|
Token::Underline(ref d) => d.render(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn flatten(&self) -> String {
|
||||||
|
match *self {
|
||||||
|
Token::Anchor(ref d) => d.flatten(),
|
||||||
|
Token::Bold(ref d) => d.flatten(),
|
||||||
|
Token::CheckBox(ref d) => d.flatten(),
|
||||||
|
Token::Code(ref d) => d.flatten(),
|
||||||
|
Token::Strike(ref d) => d.flatten(),
|
||||||
|
Token::Header(ref d) => d.flatten(),
|
||||||
|
Token::Item(ref d) => d.flatten(),
|
||||||
|
Token::LineBreak(ref d) => d.flatten(),
|
||||||
|
Token::List(ref d) => d.flatten(),
|
||||||
|
Token::Literal(ref d) => d.flatten(),
|
||||||
|
Token::Oblique(ref d) => d.flatten(),
|
||||||
|
Token::Paragraph(ref d) => d.flatten(),
|
||||||
|
Token::PreFormat(ref d) => d.flatten(),
|
||||||
|
Token::Underline(ref d) => d.flatten(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Token {
|
impl std::fmt::Display for Token {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
pub struct Anchor {
|
pub struct Anchor {
|
||||||
text: String,
|
text: String,
|
||||||
destination: Option<String>,
|
destination: Option<String>,
|
||||||
|
node_id: Option<String>,
|
||||||
node: Option<Node>,
|
node: Option<Node>,
|
||||||
leading: bool,
|
leading: bool,
|
||||||
balanced: bool,
|
balanced: bool,
|
||||||
|
|
@ -18,6 +19,7 @@ impl Anchor {
|
||||||
text: &str,
|
text: &str,
|
||||||
destination: &str,
|
destination: &str,
|
||||||
node: Option<Node>,
|
node: Option<Node>,
|
||||||
|
node_id: Option<String>,
|
||||||
leading: bool,
|
leading: bool,
|
||||||
external: bool,
|
external: bool,
|
||||||
balanced: bool,
|
balanced: bool,
|
||||||
|
|
@ -26,6 +28,7 @@ impl Anchor {
|
||||||
text: text.to_owned(),
|
text: text.to_owned(),
|
||||||
destination: Some(String::from(destination)),
|
destination: Some(String::from(destination)),
|
||||||
node,
|
node,
|
||||||
|
node_id,
|
||||||
leading,
|
leading,
|
||||||
external,
|
external,
|
||||||
balanced,
|
balanced,
|
||||||
|
|
@ -76,6 +79,14 @@ impl Anchor {
|
||||||
self.leading = leading;
|
self.leading = leading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_node(&mut self, node: &Node) {
|
||||||
|
self.node = Some(node.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_id(&self) -> Option<String> {
|
||||||
|
self.node_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn route(&mut self) {
|
fn route(&mut self) {
|
||||||
self.destination = if let Some(destination) = self.destination.clone() {
|
self.destination = if let Some(destination) = self.destination.clone() {
|
||||||
if destination.contains(":") || destination.contains("/") {
|
if destination.contains(":") || destination.contains("/") {
|
||||||
|
|
@ -83,8 +94,10 @@ impl Anchor {
|
||||||
} else if destination.is_empty() && self.text.is_empty() {
|
} else if destination.is_empty() && self.text.is_empty() {
|
||||||
None
|
None
|
||||||
} else if destination.is_empty() {
|
} else if destination.is_empty() {
|
||||||
|
self.node_id = Some(self.text.clone());
|
||||||
Some(format!("/node/{}", self.text))
|
Some(format!("/node/{}", self.text))
|
||||||
} else {
|
} else {
|
||||||
|
self.node_id = self.destination.clone();
|
||||||
Some(format!("/node/{destination}"))
|
Some(format!("/node/{destination}"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -111,7 +124,30 @@ impl Parseable for Anchor {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
format!(r#"<a href="{}">{}</a>"#, destination, &self.text)
|
let summary = if let Some(node) = self.node.clone() {
|
||||||
|
node.summary
|
||||||
|
} else {
|
||||||
|
String::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let classes = if self.node.is_some() {
|
||||||
|
String::from(r#"class="attached""#)
|
||||||
|
} else if !self.external {
|
||||||
|
String::from(r#"class="detached""#)
|
||||||
|
} else if self.external {
|
||||||
|
String::from(r#"class="external""#)
|
||||||
|
} else {
|
||||||
|
String::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"<a {classes} title="{summary}" href="{}">{}</a>"#,
|
||||||
|
destination, self.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
self.text.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,7 +203,7 @@ mod tests {
|
||||||
anchor.set_destination(Some("AnchorDest"));
|
anchor.set_destination(Some("AnchorDest"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
anchor.render(),
|
anchor.render(),
|
||||||
r#"<a href="/node/AnchorDest">AnchorText</a>"#
|
r#"<a class="detached" title="" href="/node/AnchorDest">AnchorText</a>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,20 +226,20 @@ mod tests {
|
||||||
fn token_display() {
|
fn token_display() {
|
||||||
let mut anchor = Anchor::default();
|
let mut anchor = Anchor::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Token::Anchor(anchor.clone())),
|
format!("{}", Token::Anchor(Box::new(anchor.clone()))),
|
||||||
"Tk:Anchor <empty> -> <unknown>",
|
"Tk:Anchor <empty> -> <unknown>",
|
||||||
);
|
);
|
||||||
|
|
||||||
anchor.text = String::from("FsJAt RTggA");
|
anchor.text = String::from("FsJAt RTggA");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Token::Anchor(anchor.clone())),
|
format!("{}", Token::Anchor(Box::new(anchor.clone()))),
|
||||||
"Tk:Anchor 'FsJAt RTggA' -> <unknown>",
|
"Tk:Anchor 'FsJAt RTggA' -> <unknown>",
|
||||||
);
|
);
|
||||||
|
|
||||||
anchor.text = String::from("wPVo1 0OmYm");
|
anchor.text = String::from("wPVo1 0OmYm");
|
||||||
anchor.destination = Some(String::from("M1UEp 1gbfr"));
|
anchor.destination = Some(String::from("M1UEp 1gbfr"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Token::Anchor(anchor.clone())),
|
format!("{}", Token::Anchor(Box::new(anchor.clone()))),
|
||||||
r#"Tk:Anchor 'wPVo1 0OmYm' -> "M1UEp 1gbfr""#,
|
r#"Tk:Anchor 'wPVo1 0OmYm' -> "M1UEp 1gbfr""#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -212,7 +248,7 @@ mod tests {
|
||||||
anchor.external = true;
|
anchor.external = true;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Token::Anchor(anchor.clone())),
|
format!("{}", Token::Anchor(Box::new(anchor.clone()))),
|
||||||
"Tk:Anchor 'wPVo1 0OmYm' -> \"M1UEp 1gbfr\" \
|
"Tk:Anchor 'wPVo1 0OmYm' -> \"M1UEp 1gbfr\" \
|
||||||
+Leading +Balanced +External",
|
+Leading +Balanced +External",
|
||||||
);
|
);
|
||||||
|
|
@ -230,7 +266,10 @@ mod tests {
|
||||||
let mut anchor = Anchor::default();
|
let mut anchor = Anchor::default();
|
||||||
anchor.set_text("BSThI");
|
anchor.set_text("BSThI");
|
||||||
anchor.set_destination(Some(""));
|
anchor.set_destination(Some(""));
|
||||||
assert_eq!(anchor.render(), r#"<a href="/node/BSThI">BSThI</a>"#);
|
assert_eq!(
|
||||||
|
anchor.render(),
|
||||||
|
r#"<a class="detached" title="" href="/node/BSThI">BSThI</a>"#
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ impl Parseable for Bold {
|
||||||
String::from("</strong>")
|
String::from("</strong>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Bold {
|
impl std::fmt::Display for Bold {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ impl Parseable for CheckBox {
|
||||||
let toggle = if self.checked { " checked " } else { "" };
|
let toggle = if self.checked { " checked " } else { "" };
|
||||||
format!(r#"<input type="checkbox"{toggle}/>"#)
|
format!(r#"<input type="checkbox"{toggle}/>"#)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for CheckBox {
|
impl std::fmt::Display for CheckBox {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ impl Parseable for Code {
|
||||||
String::from("</code>")
|
String::from("</code>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Code {
|
impl std::fmt::Display for Code {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,10 @@ impl Parseable for Header {
|
||||||
panic!("Attempt to render a header tag while open state is unknown")
|
panic!("Attempt to render a header tag while open state is unknown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Header {
|
impl std::fmt::Display for Header {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ impl Parseable for Item {
|
||||||
fn render(&self) -> String {
|
fn render(&self) -> String {
|
||||||
panic!("Items should only be rendered by a list's render method")
|
panic!("Items should only be rendered by a list's render method")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ impl Parseable for LineBreak {
|
||||||
fn render(&self) -> String {
|
fn render(&self) -> String {
|
||||||
"\n".to_owned()
|
"\n".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::from('\n')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for LineBreak {
|
impl std::fmt::Display for LineBreak {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ impl Parseable for List {
|
||||||
|
|
||||||
format!("\n<{tag}>\n{output}</{tag}>\n\n")
|
format!("\n<{tag}>\n{output}</{tag}>\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
format!("[List: {} items]", self.items.len())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl List {
|
impl List {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ impl Parseable for Literal {
|
||||||
fn render(&self) -> String {
|
fn render(&self) -> String {
|
||||||
self.text.clone()
|
self.text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
self.text.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Literal {
|
impl std::fmt::Display for Literal {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ impl Parseable for Oblique {
|
||||||
String::from("</em>")
|
String::from("</em>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Oblique {
|
impl std::fmt::Display for Oblique {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ impl Parseable for Paragraph {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Paragraph {
|
impl std::fmt::Display for Paragraph {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ impl Parseable for PreFormat {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ impl Parseable for Strike {
|
||||||
let tag = if self.open { "<s>" } else { "</s>" };
|
let tag = if self.open { "<s>" } else { "</s>" };
|
||||||
String::from(tag)
|
String::from(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Strike {
|
impl std::fmt::Display for Strike {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ impl Parseable for Underline {
|
||||||
String::from("</u>")
|
String::from("</u>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten(&self) -> String {
|
||||||
|
String::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Underline {
|
impl std::fmt::Display for Underline {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
syntax::command::Arguments,
|
syntax::{command::Arguments, content::parser::flatten},
|
||||||
types::{Edge, Graph, Meta, Node},
|
types::{Edge, Graph, Node},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn populate_graph() -> Graph {
|
pub fn populate_graph() -> Graph {
|
||||||
|
|
@ -16,25 +16,25 @@ pub fn populate_graph() -> Graph {
|
||||||
modulate_graph(&graph)
|
modulate_graph(&graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modulate_graph(graph: &Graph) -> Graph {
|
fn modulate_graph(in_graph: &Graph) -> Graph {
|
||||||
let nodes = modulate_nodes(&graph.nodes);
|
let nodes = modulate_nodes(in_graph);
|
||||||
|
|
||||||
Graph {
|
let mut graph = Graph {
|
||||||
incoming: make_incoming(&nodes),
|
incoming: make_incoming(&nodes),
|
||||||
lowercase_keymap: map_lowercase_keys(&nodes),
|
lowercase_keymap: map_lowercase_keys(&nodes),
|
||||||
nodes,
|
nodes,
|
||||||
meta: Meta {
|
..in_graph.to_owned()
|
||||||
config: graph.meta.config.clone().parse_text(),
|
};
|
||||||
..graph.meta.clone()
|
|
||||||
},
|
graph.parse();
|
||||||
..graph.to_owned()
|
graph
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
|
fn modulate_nodes(graph: &Graph) -> HashMap<String, Node> {
|
||||||
|
let old_nodes = graph.nodes.clone();
|
||||||
let mut nodes: HashMap<String, Node> = HashMap::default();
|
let mut nodes: HashMap<String, Node> = HashMap::default();
|
||||||
|
|
||||||
for (key, node) in old_nodes {
|
for (key, node) in old_nodes.clone() {
|
||||||
let connections = node.connections.clone().unwrap_or_default();
|
let connections = node.connections.clone().unwrap_or_default();
|
||||||
let mut new_edges = connections.clone();
|
let mut new_edges = connections.clone();
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
|
||||||
|
|
||||||
// Populate empty "from" IDs in edges with node's ID
|
// Populate empty "from" IDs in edges with node's ID
|
||||||
if edge.from.is_empty() {
|
if edge.from.is_empty() {
|
||||||
new_edge.from.clone_from(key);
|
new_edge.from.clone_from(&key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flag detached edges
|
// Flag detached edges
|
||||||
|
|
@ -62,7 +62,7 @@ fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
|
||||||
from: key.clone(),
|
from: key.clone(),
|
||||||
to: link.clone(),
|
to: link.clone(),
|
||||||
anchor: String::default(),
|
anchor: String::default(),
|
||||||
detached: !old_nodes.contains_key(link),
|
detached: !old_nodes.clone().contains_key(link),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,9 +73,25 @@ fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
|
||||||
node.title.clone()
|
node.title.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut summary = if let Some(summary) = node.text.lines().next() {
|
||||||
|
if let Some(sentence) = node.text.split_once('.') {
|
||||||
|
format!("{}.", sentence.0)
|
||||||
|
} else {
|
||||||
|
String::from(summary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.text.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if summary.len() > 300 {
|
||||||
|
summary.truncate(300);
|
||||||
|
summary.push('…');
|
||||||
|
}
|
||||||
|
|
||||||
let new_node = Node {
|
let new_node = Node {
|
||||||
id: key.clone(),
|
id: key.clone(),
|
||||||
title: new_title,
|
title: new_title,
|
||||||
|
summary: flatten(&summary, graph),
|
||||||
connections: Some(new_edges),
|
connections: Some(new_edges),
|
||||||
..node.clone()
|
..node.clone()
|
||||||
};
|
};
|
||||||
|
|
@ -177,27 +193,6 @@ mod tests {
|
||||||
let message = graph.meta.messages.first().unwrap();
|
let message = graph.meta.messages.first().unwrap();
|
||||||
assert!(message.contains("expected value at line 1 column 1"));
|
assert!(message.contains("expected value at line 1 column 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn detached_node() {
|
|
||||||
let mut node = Node::new(None);
|
|
||||||
node.connections = Some(vec![Edge {
|
|
||||||
anchor: String::from("SomeAnchor"),
|
|
||||||
from: String::default(),
|
|
||||||
to: String::default(),
|
|
||||||
detached: false,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
let mut map: HashMap<String, Node> = HashMap::default();
|
|
||||||
map.insert(String::from("SomeNode"), node);
|
|
||||||
|
|
||||||
let modulated_map = modulate_nodes(&map);
|
|
||||||
let modulated_node = modulated_map.get("SomeNode").unwrap().clone();
|
|
||||||
let modulated_connections = modulated_node.connections.unwrap();
|
|
||||||
let modulated_connection = modulated_connections.first().unwrap();
|
|
||||||
assert!(modulated_connection.anchor == "SomeAnchor");
|
|
||||||
assert!(modulated_connection.detached);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
75
src/types.rs
75
src/types.rs
|
|
@ -23,6 +23,8 @@ pub struct Node {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub text: String,
|
pub text: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub summary: String,
|
||||||
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub links: Vec<String>,
|
pub links: Vec<String>,
|
||||||
|
|
@ -107,7 +109,7 @@ pub struct Config {
|
||||||
#[serde(default = "mktrue")]
|
#[serde(default = "mktrue")]
|
||||||
pub tree: bool,
|
pub tree: bool,
|
||||||
#[serde(default = "mkfalse")]
|
#[serde(default = "mkfalse")]
|
||||||
pub tree_node_text: bool,
|
pub tree_node_summary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// See: https://github.com/serde-rs/serde/issues/368
|
// See: https://github.com/serde-rs/serde/issues/368
|
||||||
|
|
@ -121,6 +123,7 @@ fn mk8() -> u16 {
|
||||||
8
|
8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct QueryResult {
|
pub struct QueryResult {
|
||||||
pub node: Option<Node>,
|
pub node: Option<Node>,
|
||||||
pub redirect: bool,
|
pub redirect: bool,
|
||||||
|
|
@ -144,7 +147,7 @@ impl Graph {
|
||||||
pub fn find_node(&self, query: &str) -> QueryResult {
|
pub fn find_node(&self, query: &str) -> QueryResult {
|
||||||
let collapsed_query = query.trim().replace(" ", "");
|
let collapsed_query = query.trim().replace(" ", "");
|
||||||
|
|
||||||
if let Some(exact_match) = self.nodes.get(query) {
|
let candidate = if let Some(exact_match) = self.nodes.get(query) {
|
||||||
QueryResult {
|
QueryResult {
|
||||||
node: Some(exact_match.clone()),
|
node: Some(exact_match.clone()),
|
||||||
redirect: false,
|
redirect: false,
|
||||||
|
|
@ -161,12 +164,30 @@ impl Graph {
|
||||||
node: None,
|
node: None,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref candidate_node) = candidate.node
|
||||||
|
&& !candidate_node.redirect.is_empty()
|
||||||
|
{
|
||||||
|
QueryResult {
|
||||||
|
node: self.find_node(&candidate_node.redirect).node,
|
||||||
|
redirect: true,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_root(&self) -> Option<Node> {
|
pub fn get_root(&self) -> Option<Node> {
|
||||||
self.nodes.get(&self.root_node).cloned()
|
self.nodes.get(&self.root_node).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse(&mut self) {
|
||||||
|
self.meta.config.footer_text =
|
||||||
|
content::parse(&self.meta.config.footer_text, self);
|
||||||
|
self.meta.config.about_text =
|
||||||
|
content::parse(&self.meta.config.about_text, self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
|
|
@ -182,17 +203,7 @@ impl Node {
|
||||||
links: vec![],
|
links: vec![],
|
||||||
redirect: String::default(),
|
redirect: String::default(),
|
||||||
hidden: false,
|
hidden: false,
|
||||||
}
|
summary: String::default(),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
#[must_use]
|
|
||||||
pub fn parse_text(self) -> Config {
|
|
||||||
Config {
|
|
||||||
footer_text: content::parse(&self.footer_text, &self),
|
|
||||||
about_text: content::parse(&self.about_text, &self),
|
|
||||||
..self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +233,7 @@ impl Default for Config {
|
||||||
site_description: String::default(),
|
site_description: String::default(),
|
||||||
site_title: String::default(),
|
site_title: String::default(),
|
||||||
tree: true,
|
tree: true,
|
||||||
tree_node_text: false,
|
tree_node_summary: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -252,33 +263,35 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_footer_text() {
|
fn empty_footer_text() {
|
||||||
let default_graph = populate_graph();
|
let mut graph = populate_graph();
|
||||||
|
|
||||||
let config = Config {
|
graph.meta.config = Config {
|
||||||
footer_text: String::default(),
|
footer_text: String::default(),
|
||||||
..default_graph.meta.config
|
..graph.meta.config
|
||||||
};
|
};
|
||||||
|
|
||||||
let parsed_config = config.parse_text();
|
graph.parse();
|
||||||
|
|
||||||
println!("{:?}", parsed_config.footer_text);
|
println!("{:?}", graph.meta.config.footer_text);
|
||||||
assert!(parsed_config.footer_text.is_empty());
|
assert!(graph.meta.config.footer_text.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_footer_text() {
|
fn config_footer_text() {
|
||||||
let payload = "0kqBrdS8NPrU4xVxh2xW0hUzAw926JCQ";
|
let payload = "0kqBrdS8NPrU4xVxh2xW0hUzAw926JCQ";
|
||||||
let default_graph = populate_graph();
|
let mut graph = populate_graph();
|
||||||
|
|
||||||
let config = Config {
|
graph.meta.config = Config {
|
||||||
footer_text: format!("`{payload}`"),
|
footer_text: format!("`{payload}`"),
|
||||||
..default_graph.meta.config
|
..graph.meta.config
|
||||||
};
|
};
|
||||||
|
|
||||||
let parsed_config = config.parse_text();
|
graph.parse();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
parsed_config
|
graph
|
||||||
|
.meta
|
||||||
|
.config
|
||||||
.footer_text
|
.footer_text
|
||||||
.matches(format!("<code>{payload}</code>").as_str())
|
.matches(format!("<code>{payload}</code>").as_str())
|
||||||
.count()
|
.count()
|
||||||
|
|
@ -289,17 +302,19 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn config_about_text() {
|
fn config_about_text() {
|
||||||
let payload = "ZqPFl84JlzSS0QUo61RwTUPONIE78Lmw";
|
let payload = "ZqPFl84JlzSS0QUo61RwTUPONIE78Lmw";
|
||||||
let default_graph = populate_graph();
|
let mut graph = populate_graph();
|
||||||
|
|
||||||
let config = Config {
|
graph.meta.config = Config {
|
||||||
about_text: format!("`{payload}`"),
|
about_text: format!("`{payload}`"),
|
||||||
..default_graph.meta.config
|
..graph.meta.config
|
||||||
};
|
};
|
||||||
|
|
||||||
let parsed_config = config.parse_text();
|
graph.parse();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
parsed_config
|
graph
|
||||||
|
.meta
|
||||||
|
.config
|
||||||
.about_text
|
.about_text
|
||||||
.matches(format!("<code>{payload}</code>").as_str())
|
.matches(format!("<code>{payload}</code>").as_str())
|
||||||
.count()
|
.count()
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,7 @@ en is only possible thanks to a number of projects and people:
|
||||||
[nodes.Roadmap]
|
[nodes.Roadmap]
|
||||||
text = """
|
text = """
|
||||||
- [x] Redirects
|
- [x] Redirects
|
||||||
- [ ] Strip/render some syntax in Tree text preview
|
- [x] Strip/render some syntax in Tree text preview
|
||||||
- [x] Drop-down navigation
|
- [x] Drop-down navigation
|
||||||
- [ ] Meta-awareness
|
- [ ] Meta-awareness
|
||||||
- [ ] Detached edges
|
- [ ] Detached edges
|
||||||
|
|
@ -540,7 +540,7 @@ text = """
|
||||||
- [ ] By most linked to
|
- [ ] By most linked to
|
||||||
- [ ] By most linked
|
- [ ] By most linked
|
||||||
- [x] Anchors and connections
|
- [x] Anchors and connections
|
||||||
- [ ] Render detached anchors differently
|
- [x] Render detached anchors differently
|
||||||
- [ ] Suffix-aware anchors
|
- [ ] Suffix-aware anchors
|
||||||
- [x] Plural anchors (`|node|s` -> `node`)
|
- [x] Plural anchors (`|node|s` -> `node`)
|
||||||
- [x] Ignore trailing punctuation
|
- [x] Ignore trailing punctuation
|
||||||
|
|
@ -560,6 +560,7 @@ text = """
|
||||||
- [x] External anchors
|
- [x] External anchors
|
||||||
- [x] Richer text formatting
|
- [x] Richer text formatting
|
||||||
- [x] Nested formatting
|
- [x] Nested formatting
|
||||||
|
- [ ] Blockquotes
|
||||||
- [x] Headers
|
- [x] Headers
|
||||||
- [x] Preformatted blocks
|
- [x] Preformatted blocks
|
||||||
- [x] _Oblique_,
|
- [x] _Oblique_,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,12 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
text-decoration-color: #aaa;
|
text-decoration-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.detached {
|
||||||
|
color: #595959;
|
||||||
|
text-decoration-color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.header-row {
|
div.header-row {
|
||||||
|
|
@ -75,24 +80,6 @@ h1.node-title {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.tree-node-text {
|
|
||||||
display: inline;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.tree-node-text {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.tree-node-text {
|
|
||||||
display: inline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.tree-node-text {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer div {
|
footer div {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -180,6 +167,11 @@ em#index-node-count {
|
||||||
text-decoration-color: #159b9b;
|
text-decoration-color: #159b9b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.detached {
|
||||||
|
color: #acacac;
|
||||||
|
text-decoration-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
span.id-label {
|
span.id-label {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
|
|
|
||||||
|
|
@ -15,28 +15,20 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/node/{{root_node.id}}">{{root_node.title}}</a>
|
<a href="/node/{{root_node.id}}">{{root_node.title}}</a>
|
||||||
{% if root_node.connections or config.tree_node_text %}
|
{% if root_node.connections or config.tree_node_summary %}
|
||||||
<ul>
|
<ul>
|
||||||
{% if config.tree_node_text %}
|
{% if config.tree_node_summary %}
|
||||||
<li><strong>Text:</strong>
|
<li class="tree-node-summary">
|
||||||
<ul class="tree-node-text">
|
{{ root_node.summary }}
|
||||||
<li class="tree-node-text">
|
|
||||||
<details class="tree-node-text">
|
|
||||||
<summary class="tree-node-text">
|
|
||||||
{{root_node.text | truncate(length=120)}}
|
|
||||||
</summary>
|
|
||||||
{{root_node.text}}
|
|
||||||
</details>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if root_node.connections %}
|
{% if root_node.connections %}
|
||||||
{% if config.tree_node_text %}<li><strong>Connections</strong>
|
{% if config.tree_node_summary %}<li><strong>Connections</strong>
|
||||||
<ul>{% endif %}
|
<ul>{% endif %}
|
||||||
{% for connection in root_node.connections %}
|
{% for connection in root_node.connections %}
|
||||||
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if config.tree_node_text %}</ul>
|
{% if config.tree_node_summary %}</ul>
|
||||||
</li>{% endif %}
|
</li>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -51,30 +43,22 @@
|
||||||
{% for node in nodes | filter(attribute="hidden", value=false)%}
|
{% for node in nodes | filter(attribute="hidden", value=false)%}
|
||||||
<li>
|
<li>
|
||||||
<a href="/node/{{node.id}}">{{node.title}}</a>
|
<a href="/node/{{node.id}}">{{node.title}}</a>
|
||||||
{% if node.connections or config.tree_node_text %}
|
{% if node.connections or config.tree_node_summary %}
|
||||||
<ul>
|
<ul>
|
||||||
{% if config.tree_node_text %}
|
{% if config.tree_node_summary %}
|
||||||
<li><strong>Text:</strong>
|
<li class="tree-node-summary">
|
||||||
<ul class="tree-node-text">
|
{{node.summary}}
|
||||||
<li class="tree-node-text">
|
|
||||||
<details class="tree-node-text">
|
|
||||||
<summary class="tree-node-text">
|
|
||||||
{{node.text | truncate(length=30)}}
|
|
||||||
</summary>
|
|
||||||
{{node.text}}
|
|
||||||
</details>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if node.connections %}
|
{% if node.connections %}
|
||||||
{% if config.tree_node_text %}<li><strong>Connections</strong>
|
{% if config.tree_node_summary %}<li><strong>Connections</strong>
|
||||||
<ul>{% endif %}
|
<ul>{% endif %}
|
||||||
{% for connection in node.connections %}
|
{% for connection in node.connections %}
|
||||||
{% if not connection.detached %}
|
{% if not connection.detached %}
|
||||||
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
<li><a href="/node/{{connection.to}}">{{connection.to}}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if config.tree_node_text %}</ul>
|
{% if config.tree_node_summary %}</ul>
|
||||||
</li>{% endif %}
|
</li>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue