From 88fdd3084e3601080e8bc8aa538d867ce653af82 Mon Sep 17 00:00:00 2001
From: jutty
Date: Wed, 3 Jun 2026 21:23:40 -0300
Subject: [PATCH] Flag absolute anchors
---
Cargo.lock | 8 ++--
src/graph.rs | 2 +-
src/syntax/content/parser/context/anchor.rs | 49 +++++++++++++++++----
src/syntax/content/parser/token/anchor.rs | 49 ++++++++++++++++-----
static/public/assets/style.css | 13 ++++++
templates/data.html | 7 +--
6 files changed, 101 insertions(+), 27 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2d09c10..daa0607 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/src/graph.rs b/src/graph.rs
index 0fd940c..9cc62a5 100644
--- a/src/graph.rs
+++ b/src/graph.rs
@@ -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/")
diff --git a/src/syntax/content/parser/context/anchor.rs b/src/syntax/content/parser/context/anchor.rs
index 0fbebb6..2aa172c 100644
--- a/src/syntax/content/parser/context/anchor.rs
+++ b/src/syntax/content/parser/context/anchor.rs
@@ -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#"Node
"#)
+ concat!(
+ r#"Node
"#
+ )
);
}
@@ -225,9 +237,11 @@ mod tests {
fn anchor_to_node_s() {
assert_eq!(
read("The |letter s|s|'s node: |s|!"),
- concat!(r#"The The letter s's node: "#,
- r#"s!
"#)
+ r#"s!
"#
+ )
);
}
@@ -246,9 +260,11 @@ mod tests {
fn leading_plural_anchor() {
assert_eq!(
read("Interfaces are |element|s of |system|s."),
- concat!(r#"Interfaces are Interfaces are elements of systems.
"#)
+ r#"title="" href="/node/system">systems."#
+ )
);
}
@@ -268,9 +284,11 @@ mod tests {
fn explicit_end_of_destination() {
assert_eq!(
read("interactions are |basic elements|BasicElements| of systems"),
- concat!(r#"interactions are interactions are basic elements of "#,
- r#"systems
"#)
+ r#"systems"#
+ )
);
}
@@ -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#"see the "#,
+ r#"raw endpoints.
"#,
+ ),
+ );
+ }
+
#[test]
fn http_external_anchor() {
assert_eq!(
diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs
index 1efecf9..556ac42 100644
--- a/src/syntax/content/parser/token/anchor.rs
+++ b/src/syntax/content/parser/token/anchor.rs
@@ -11,6 +11,7 @@ pub struct Anchor {
node: Option,
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#"{}"#,
- destination, self.text,
+ r#"{text}"#
)
}
diff --git a/static/public/assets/style.css b/static/public/assets/style.css
index d3ef9b5..8afbacd 100644
--- a/static/public/assets/style.css
+++ b/static/public/assets/style.css
@@ -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
diff --git a/templates/data.html b/templates/data.html
index f7a1bca..3a18a7f 100644
--- a/templates/data.html
+++ b/templates/data.html
@@ -9,15 +9,16 @@
-Detached edges
+Detached connections
+These are the destinations to which connections exist, but the target node doesn't.
Expand to see all detached edges.