From 6c743c1d5fb62bef6466c23b4b31cd9dc10a156b Mon Sep 17 00:00:00 2001 From: Yauhen Yakimovich Date: Mon, 28 Sep 2026 19:18:05 +0200 Subject: [PATCH] fix(document): a wikilink's display text is not part of its target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[[link-op|link operator]]` writes one reference, and the words after the bar are there so the sentence reads naturally. The collector took the whole bracketed string as the target, so that link produced a node named `link-op|link operator` beside the real `link-op` — thirty-three such links across `doc/`, fourteen distinct phantom nodes. Splitting at the first bar is the whole fix. Duplicates need no work: `build_edges` already identifies a link edge by its endpoints, so `[[beta]]` and `[[beta|the second one]]` now collapse into one edge with their annotations merged, which is what the design asked for and what the wrong target was preventing. --- src/document.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/document.rs b/src/document.rs index 81a593f..a814d1f 100644 --- a/src/document.rs +++ b/src/document.rs @@ -265,6 +265,9 @@ fn heading(body: &str) -> Option { } /// Collect `[[target]]` references and the `{..}` annotations that follow them. +/// +/// A reference may carry display text as `[[target|display text]]`, which names +/// nothing: the target is what precedes the bar. fn references(body: &str) -> Result, Error> { let mut found = Vec::new(); let bytes = body.as_bytes(); @@ -274,7 +277,15 @@ fn references(body: &str) -> Result, Error> { let Some(length) = body[open..].find("]]") else { break; }; - let target = body[open..open + length].trim().to_owned(); + // Display text declares nothing: `[[alice|Alice B.]]` references + // `alice`, and the rest is there so the sentence reads naturally. A + // later bar is display text too, so only the first one separates. + let written = &body[open..open + length]; + let target = written + .split_once('|') + .map_or(written, |(target, _)| target) + .trim() + .to_owned(); offset = open + length + 2; if target.is_empty() { continue; @@ -410,6 +421,41 @@ mod tests { assert_eq!(doc.references[1].data, Data::map()); } + #[test] + fn display_text_is_not_part_of_a_reference_target() { + let doc = document("see [[link-op|the link operator]] and [[alice | Alice B.]].\n"); + let names: Vec<&str> = doc + .references + .iter() + .map(|reference| reference.target.as_str()) + .collect(); + assert_eq!(names, ["link-op", "alice"]); + } + + #[test] + fn display_text_does_not_hide_an_annotation() { + let doc = document("[[bob|Bob]] {relation: parent}\n"); + assert_eq!(doc.references[0].target, "bob"); + assert_eq!( + doc.references[0] + .data + .path("relation") + .and_then(Data::as_str), + Some("parent") + ); + } + + #[test] + fn a_reference_that_is_only_display_text_names_nothing() { + let doc = document("[[|just words]] and [[alice]]\n"); + let names: Vec<&str> = doc + .references + .iter() + .map(|reference| reference.target.as_str()) + .collect(); + assert_eq!(names, ["alice"]); + } + #[test] fn a_document_without_a_header_keeps_its_whole_body() { let doc = document("just text\n");