diff --git a/src/syntax/content/parser/context/anchor.rs b/src/syntax/content/parser/context/anchor.rs index 077e1f9..8843ab4 100644 --- a/src/syntax/content/parser/context/anchor.rs +++ b/src/syntax/content/parser/context/anchor.rs @@ -23,7 +23,7 @@ pub fn parse( // This is only true if the anchor is leading, otherwise the outer parser // would already have set its text to the word before the first pipe - if candidate.text.is_empty() { + if candidate.text().is_empty() { log!( "Seeking end of text at {:#?} -> {:#?}", lexeme.text(), @@ -32,7 +32,7 @@ pub fn parse( if lexeme.next() == "|" { log!("End: Next lexeme is a pipe"); buffer.text.push_str(&lexeme.text()); - candidate.text.clone_from(&buffer.text); + candidate.set_text(&buffer.text.clone()); } else { log!( "Pushing non-terminal {:#?} into buffer {:#?}", @@ -44,7 +44,7 @@ pub fn parse( return true; } - if candidate.destination.is_none() { + if candidate.destination().is_none() { log!( "Seeking end of destination at {:#?} -> {:#?}", lexeme.text(), @@ -58,8 +58,8 @@ pub fn parse( && !lexeme.match_next_char('|') { log!("End: Plural anchor"); - candidate.destination = Some(candidate.text.clone()); - candidate.text.push('s'); + candidate.set_destination(Some(&candidate.text().clone())); + candidate.text_push("s"); if lexeme.last() { tokens.push(Token::Anchor(candidate.clone())); state.context.inline = Inline::None; @@ -68,43 +68,43 @@ pub fn parse( } else if lexeme.match_char('|') && lexeme.is_next_delimiter() { log!("End: Pipe followed by delimiter"); if buffer.destination.is_empty() { - candidate.destination = Some(candidate.text.clone()); + candidate.set_destination(Some(&candidate.text().clone())); } else { - candidate.destination = Some(buffer.destination.clone()); + candidate.set_destination(Some(&buffer.destination.clone())); } tokens.push(Token::Anchor(candidate.clone())); state.context.inline = Inline::None; 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"); - candidate.balanced = true; + candidate.set_balanced(true); return true; } else if lexeme.match_char(':') { log!("State: Found a colon, marking anchor as external"); - candidate.external = true; + candidate.set_external(true); buffer.destination.push_str(&lexeme.text()); return true; } else if lexeme.match_char('|') { log!("End: Explicit end-of-destination pipe"); - candidate.destination = Some(buffer.destination.clone()); + candidate.set_destination(Some(&buffer.destination.clone())); return true; - } else if !candidate.external && lexeme.is_delimiter() { + } else if !candidate.external() && lexeme.is_delimiter() { log!("End: Internal anchor trailed by delimiter"); - candidate.destination = Some(buffer.destination.clone()); + candidate.set_destination(Some(&buffer.destination.clone())); tokens.push(Token::Anchor(candidate.clone())); state.context.inline = Inline::None; return false; } else if lexeme.is_next_whitespace() { log!("End: next is whitespace"); buffer.destination.push_str(&lexeme.text()); - candidate.destination = Some(buffer.destination.clone()); + candidate.set_destination(Some(&buffer.destination.clone())); tokens.push(Token::Anchor(candidate.clone())); state.context.inline = Inline::None; return true; } else if lexeme.last() { log!("End: end of input"); buffer.destination.push_str(&lexeme.text()); - candidate.destination = Some(buffer.destination.clone()); + candidate.set_destination(Some(&buffer.destination.clone())); tokens.push(Token::Anchor(candidate.clone())); state.context.inline = Inline::None; return true; @@ -119,7 +119,7 @@ pub fn parse( ); buffer.destination.push_str(&lexeme.text()); if lexeme.last() { - candidate.destination = Some(buffer.destination.clone()); + candidate.set_destination(Some(&buffer.destination.clone())); tokens.push(Token::Anchor(candidate.clone())); state.context.inline = Inline::None; } @@ -132,7 +132,7 @@ pub fn parse( // was never found and we kept filling the buffer endlessly, // causing the program to panic anyways when rendering anchors assert!( - candidate.destination.is_some(), + candidate.destination().is_some(), "Anchor context parsing done but no destination found: {:#?}", state.buffers.anchor ); @@ -149,6 +149,11 @@ mod tests { parser::read(input, &Graph::new(None).meta.config) } + #[test] + fn flanking() { + assert_eq!(read("|Node|"), r#"

Node

"#); + } + #[test] fn flanking_with_trailing_comma() { assert_eq!(read("|Node|,"), r#"

Node,

"#); diff --git a/src/syntax/content/parser/context/inline.rs b/src/syntax/content/parser/context/inline.rs index 7d5633d..7b05af7 100644 --- a/src/syntax/content/parser/context/inline.rs +++ b/src/syntax/content/parser/context/inline.rs @@ -32,9 +32,9 @@ pub fn parse( state.buffers.anchor = AnchorBuffer::default(); if lexeme.match_char('|') { - state.buffers.anchor.candidate.leading = true; + state.buffers.anchor.candidate.set_leading(true); } else { - state.buffers.anchor.candidate.text = lexeme.text(); + state.buffers.anchor.candidate.set_text(&lexeme.text()); // because we probed positively and this is not a pipe, // the next lexeme must be and so it was now parsed iterator.next(); diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs index d079ac6..25481dd 100644 --- a/src/syntax/content/parser/token/anchor.rs +++ b/src/syntax/content/parser/token/anchor.rs @@ -1,12 +1,96 @@ -use crate::syntax::content::{Parseable, parser::lexeme::Lexeme}; +use crate::{ + syntax::content::{Parseable, parser::lexeme::Lexeme}, + types::Node, +}; -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Default, Debug, Clone, Eq, PartialEq)] pub struct Anchor { - pub text: String, - pub destination: Option, - pub leading: bool, - pub balanced: bool, - pub external: bool, + text: String, + destination: Option, + node: Option, + leading: bool, + balanced: bool, + external: bool, +} + +impl Anchor { + pub fn new( + text: &str, + destination: &str, + node: Option, + leading: bool, + external: bool, + balanced: bool, + ) -> Anchor { + let mut anchor = Anchor { + text: text.to_owned(), + destination: Some(String::from(destination)), + node, + leading, + external, + balanced, + }; + + anchor.route(); + anchor + } + + pub fn text(&self) -> String { + self.text.clone() + } + + pub fn set_text(&mut self, text: &str) { + self.text = String::from(text); + } + + pub fn text_push(&mut self, text: &str) { + self.text.push_str(text); + } + + pub fn destination(&self) -> Option { + self.destination.clone() + } + + pub fn set_destination(&mut self, destination: Option<&str>) { + self.destination = destination.map(str::to_string); + self.route(); + } + + pub fn balanced(&self) -> bool { + self.balanced + } + + pub fn set_balanced(&mut self, balanced: bool) { + self.balanced = balanced; + } + + pub fn external(&self) -> bool { + self.external + } + + pub fn set_external(&mut self, external: bool) { + self.external = external; + } + + pub fn set_leading(&mut self, leading: bool) { + self.leading = leading; + } + + fn route(&mut self) { + self.destination = if let Some(destination) = self.destination.clone() { + if destination.contains(":") || destination.contains("/") { + Some(destination) + } else if destination.is_empty() && self.text.is_empty() { + None + } else if destination.is_empty() { + Some(format!("/node/{}", self.text)) + } else { + Some(format!("/node/{destination}")) + } + } else { + None + } + } } impl Parseable for Anchor { @@ -27,17 +111,7 @@ impl Parseable for Anchor { ) }; - let non_empty_destination = if destination.is_empty() { - self.text.clone() - } else { - destination.to_owned() - }; - - format!( - r#"{}"#, - Anchor::resolve_destination(&non_empty_destination), - &self.text - ) + format!(r#"{}"#, destination, &self.text) } } @@ -79,32 +153,6 @@ impl std::fmt::Display for Anchor { } } -impl Anchor { - pub fn new( - text: &str, - destination: &str, - leading: bool, - external: bool, - balanced: bool, - ) -> Anchor { - Anchor { - text: text.to_owned(), - destination: Some(Anchor::resolve_destination(destination)), - leading, - external, - balanced, - } - } - - fn resolve_destination(raw: &str) -> String { - if raw.contains(":") || raw.contains("/") { - raw.to_owned() - } else { - format!("/node/{raw}") - } - } -} - #[cfg(test)] mod tests { @@ -114,8 +162,9 @@ mod tests { #[test] fn render_anchor() { - let anchor = - Anchor::new("AnchorText", "AnchorDest", true, false, false); + let mut anchor = Anchor::default(); + anchor.set_text("AnchorText"); + anchor.set_destination(Some("AnchorDest")); assert_eq!( anchor.render(), r#"AnchorText"# @@ -168,4 +217,27 @@ mod tests { +Leading +Balanced +External", ); } + + #[test] + fn display_empty_destination() { + let mut anchor = Anchor::default(); + anchor.set_destination(Some("")); + assert_eq!(format!("{anchor}"), "Anchor -> "); + } + + #[test] + fn render_empty_destination() { + let mut anchor = Anchor::default(); + anchor.set_text("BSThI"); + anchor.set_destination(Some("")); + assert_eq!(anchor.render(), r#"BSThI"#); + } + + #[test] + fn resolve_none_destination() { + let mut anchor = Anchor::default(); + anchor.set_destination(None); + anchor.route(); // set_destination also called this + assert!(anchor.destination().is_none()); + } } diff --git a/static/graph.toml b/static/graph.toml index a67c91d..e433e38 100644 --- a/static/graph.toml +++ b/static/graph.toml @@ -202,7 +202,7 @@ DRC|DemocraticRepublicOfTheCongo docs|https://en.jutty.dev/node/Documentation ` -As shown above, anchors can point to external addresses. These are identified by the presence of a `:` character in the destination. Otherwise, the anchor will point to a node with an ID matching the destination. +As shown above, anchors can point to external addresses. These are identified by the presence of either a `:` or a `/` character in the destination. Otherwise, the anchor will point to a node with an ID matching the destination. This means your anchors to external URLs, special handlers such as `mailto:user@domain.com` and destinations relative to the website root like `/about` will all work as intended without being interpreted as node IDs. If the left side contains spaces, you need a leading `|` character: