Flag absolute anchors

This commit is contained in:
Juno Takano 2026-06-03 21:23:40 -03:00
commit 88fdd3084e
6 changed files with 100 additions and 26 deletions

8
Cargo.lock generated
View file

@ -139,9 +139,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"num-traits",
@ -531,9 +531,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "log"
version = "0.4.31"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "matchit"

View file

@ -396,7 +396,7 @@ impl Graph {
);
} else {
if let Some(destination) = anchor.destination()
&& !anchor.external()
&& !anchor.absolute()
{
let trimmed_destination = destination
.trim_start_matches("/node/")

View file

@ -34,6 +34,9 @@ pub fn parse(
log!(VERBOSE, "End: Next lexeme is a pipe");
buffer.text.push_str(&lexeme.text());
candidate.set_text(&buffer.text.clone());
if buffer.text.starts_with('/') {
candidate.set_absolute(true);
}
} else {
log!(
VERBOSE,
@ -84,6 +87,13 @@ pub fn parse(
"State: Found a pipe, but no boundary: destination follows"
);
candidate.set_balanced(true);
if lexeme.match_next_first_char('/') {
log!(
VERBOSE,
"State: Destination starts with a dash, marking as absolute"
);
candidate.set_absolute(true);
}
return true;
} else if lexeme.match_char(':') {
log!(VERBOSE, "State: Found a colon, marking anchor as external");
@ -205,8 +215,10 @@ mod tests {
fn needless_three_pipe_anchor() {
assert_eq!(
read("|Node|Destination|"),
concat!(r#"<p><a class="detached" title="" "#,
r#"href="/node/Destination">Node</a></p>"#)
concat!(
r#"<p><a class="detached" title="" "#,
r#"href="/node/Destination">Node</a></p>"#
)
);
}
@ -225,9 +237,11 @@ mod tests {
fn anchor_to_node_s() {
assert_eq!(
read("The |letter s|s|'s node: |s|!"),
concat!(r#"<p>The <a class="detached" title="" "#,
concat!(
r#"<p>The <a class="detached" title="" "#,
r#"href="/node/s">letter s</a>'s node: "#,
r#"<a class="detached" title="" href="/node/s">s</a>!</p>"#)
r#"<a class="detached" title="" href="/node/s">s</a>!</p>"#
)
);
}
@ -246,9 +260,11 @@ mod tests {
fn leading_plural_anchor() {
assert_eq!(
read("Interfaces are |element|s of |system|s."),
concat!(r#"<p>Interfaces are <a class="detached" title="" "#,
concat!(
r#"<p>Interfaces are <a class="detached" title="" "#,
r#"href="/node/element">elements</a> of <a class="detached" "#,
r#"title="" href="/node/system">systems</a>.</p>"#)
r#"title="" href="/node/system">systems</a>.</p>"#
)
);
}
@ -268,9 +284,11 @@ mod tests {
fn explicit_end_of_destination() {
assert_eq!(
read("interactions are |basic elements|BasicElements| of systems"),
concat!(r#"<p>interactions are <a class="detached" title="" "#,
concat!(
r#"<p>interactions are <a class="detached" title="" "#,
r#"href="/node/BasicElements">basic elements</a> of "#,
r#"systems</p>"#)
r#"systems</p>"#
)
);
}
@ -327,6 +345,21 @@ mod tests {
);
}
#[test]
fn absolute_anchor() {
let parse_result =
parser::rich_read("see the |raw endpoints|/data|.", &Graph::load());
println!("Parsed tokens: {:#?}", parse_result.tokens);
assert_eq!(
parse_result.text.unwrap(),
concat!(
r#"<p>see the <a class="absolute" title="" "#,
r#"href="/data">"#,
r#"raw endpoints</a>.</p>"#,
),
);
}
#[test]
fn http_external_anchor() {
assert_eq!(

View file

@ -11,6 +11,7 @@ pub struct Anchor {
node: Option<Node>,
leading: bool,
balanced: bool,
absolute: bool,
external: bool,
}
@ -34,10 +35,17 @@ impl Anchor {
self.balanced = balanced;
}
pub const fn absolute(&self) -> bool { self.absolute }
pub const fn set_absolute(&mut self, absolute: bool) {
self.absolute = absolute;
}
pub const fn external(&self) -> bool { self.external }
pub const fn set_external(&mut self, external: bool) {
self.external = external;
self.absolute = true;
}
pub const fn set_leading(&mut self, leading: bool) {
@ -58,21 +66,28 @@ impl Anchor {
fn route(&mut self) {
self.destination = if let Some(destination) = self.destination.clone() {
if destination.contains(':') || destination.contains('/') {
if destination.contains(':') || destination.starts_with('/') {
Some(destination)
} else if destination.is_empty() && self.text.is_empty() {
None
} else if destination.is_empty() {
self.node_id = Some(self.text.clone());
self.node_id = Some(Self::strip_fragment(&self.text));
Some(format!("/node/{}", self.text))
} else {
self.node_id = self.destination.clone();
self.node_id = self
.destination
.clone()
.map(|d| Anchor::strip_fragment(&d));
Some(format!("/node/{destination}"))
}
} else {
None
}
}
fn strip_fragment(target: &str) -> String {
target.split('#').next().unwrap_or(target).to_string()
}
}
impl Parseable for Anchor {
@ -100,19 +115,31 @@ impl Parseable for Anchor {
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 {
let classes = if self.external {
String::from(r#"class="external""#)
} else if self.absolute {
String::from(r#"class="absolute""#)
} else if self.node.is_some() {
String::from(r#"class="attached""#)
} else {
String::default()
String::from(r#"class="detached""#)
};
let text = if destination.contains('#')
&& !self.absolute
&& destination == &format!("/node/{}", self.text)
{
self.text
.split('#')
.next()
.unwrap_or(&self.text)
.to_string()
} else {
self.text.clone()
};
format!(
r#"<a {classes} title="{summary}" href="{}">{}</a>"#,
destination, self.text,
r#"<a {classes} title="{summary}" href="{destination}">{text}</a>"#
)
}

View file

@ -99,6 +99,19 @@ a.external:hover {
transition: 1500ms;
}
a.absolute {
color: light-dark(#196719, #2fe471);
text-decoration-color: light-dark(#196719, #2fe471);
text-decoration-style: solid;
text-decoration-thickness: 1.5px;
transition: 1500ms;
}
a.absolute:hover {
filter: brightness(1.25);
transition: 1500ms;
}
a:visited,
a.detached:visited,
a.external:visited

View file

@ -9,15 +9,16 @@
<table>
<tr>
<td>
<a href="#detached-edges">Detached edges</a>
<a href="#detached-connections">Detached connections</a>
</td>
<td>
<a href="#detached-edges:~:text=Anchors">{{ detached_count }}</a>
<a href="#detached-connections:~:text=Anchors">{{ detached_count }}</a>
</td>
</tr>
</table>
<h3 id="detached-edges">Detached edges</h3>
<h3 id="detached-connections">Detached connections</h3>
<p>These are the destinations to which connections exist, but the target node doesn't.</p>
<details>
<summary>Expand to see all detached edges.</summary>