Make anchor token fields private

This commit is contained in:
Juno Takano 2026-01-11 00:38:51 -03:00
commit eb96b456ef
4 changed files with 143 additions and 66 deletions

View file

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

View file

@ -32,9 +32,9 @@ pub fn parse(
state.buffers.anchor = AnchorBuffer::default(); state.buffers.anchor = AnchorBuffer::default();
if lexeme.match_char('|') { if lexeme.match_char('|') {
state.buffers.anchor.candidate.leading = true; state.buffers.anchor.candidate.set_leading(true);
} else { } 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, // because we probed positively and this is not a pipe,
// the next lexeme must be and so it was now parsed // the next lexeme must be and so it was now parsed
iterator.next(); iterator.next();

View file

@ -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 struct Anchor {
pub text: String, text: String,
pub destination: Option<String>, destination: Option<String>,
pub leading: bool, node: Option<Node>,
pub balanced: bool, leading: bool,
pub external: bool, balanced: bool,
external: bool,
}
impl Anchor {
pub fn new(
text: &str,
destination: &str,
node: Option<Node>,
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<String> {
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 { impl Parseable for Anchor {
@ -27,17 +111,7 @@ impl Parseable for Anchor {
) )
}; };
let non_empty_destination = if destination.is_empty() { format!(r#"<a href="{}">{}</a>"#, destination, &self.text)
self.text.clone()
} else {
destination.to_owned()
};
format!(
r#"<a href="{}">{}</a>"#,
Anchor::resolve_destination(&non_empty_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)] #[cfg(test)]
mod tests { mod tests {
@ -114,8 +162,9 @@ mod tests {
#[test] #[test]
fn render_anchor() { fn render_anchor() {
let anchor = let mut anchor = Anchor::default();
Anchor::new("AnchorText", "AnchorDest", true, false, false); anchor.set_text("AnchorText");
anchor.set_destination(Some("AnchorDest"));
assert_eq!( assert_eq!(
anchor.render(), anchor.render(),
r#"<a href="/node/AnchorDest">AnchorText</a>"# r#"<a href="/node/AnchorDest">AnchorText</a>"#
@ -168,4 +217,27 @@ mod tests {
+Leading +Balanced +External", +Leading +Balanced +External",
); );
} }
#[test]
fn display_empty_destination() {
let mut anchor = Anchor::default();
anchor.set_destination(Some(""));
assert_eq!(format!("{anchor}"), "Anchor <empty> -> <unknown>");
}
#[test]
fn render_empty_destination() {
let mut anchor = Anchor::default();
anchor.set_text("BSThI");
anchor.set_destination(Some(""));
assert_eq!(anchor.render(), r#"<a href="/node/BSThI">BSThI</a>"#);
}
#[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());
}
} }

View file

@ -202,7 +202,7 @@ DRC|DemocraticRepublicOfTheCongo
docs|https://en.jutty.dev/node/Documentation 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: If the left side contains spaces, you need a leading `|` character: