use sealmap_model::{Fingerprint, IdError, SymbolId};
use serde::Serialize;
pub const POINTER_KEY: &str = "sealed";
struct FrontMatter<'a> {
lines: Vec<&'a str>,
end: usize,
}
fn normalise(text: &str) -> std::borrow::Cow<'_, str> {
if text.contains('\r') { text.replace("\r\n", "\n").replace('\r', "\n").into() } else { text.into() }
}
fn front_matter(text: &str) -> Option<FrontMatter<'_>> {
let rest = text.strip_prefix("---\n")?;
let mut offset = 4;
let mut lines = Vec::new();
for line in rest.split_inclusive('\n') {
offset += line.len();
let bare = line.strip_suffix('\n').unwrap_or(line);
if bare == "---" {
return Some(FrontMatter { lines, end: offset });
}
lines.push(bare);
}
None
}
fn key_value<'a>(line: &'a str, key: &str) -> Option<&'a str> {
let rest = line.strip_prefix(key)?.strip_prefix(':')?;
Some(unquote(rest.trim()))
}
fn unquote(v: &str) -> &str {
for q in ['"', '\''] {
if v.len() >= 2 && v.starts_with(q) && v.ends_with(q) {
return &v[1..v.len() - 1];
}
}
v
}
pub fn topic_id(text: &str) -> Option<String> {
let text = normalise(text);
let fm = front_matter(&text)?;
fm.lines.iter().find_map(|l| key_value(l, "id")).filter(|v| !v.is_empty()).map(str::to_owned)
}
pub fn pointers(text: &str) -> Vec<String> {
let text = normalise(text);
front_matter(&text)
.map(|fm| fm.lines.iter().filter_map(|l| key_value(l, POINTER_KEY)).map(str::to_owned).collect())
.unwrap_or_default()
}
pub fn topic_hash(text: &str) -> Fingerprint {
let text = normalise(text);
let hashed = without_pointer(&text);
Fingerprint::from_blake3(&blake3::hash(hashed.as_bytes()))
}
fn without_pointer(text: &str) -> std::borrow::Cow<'_, str> {
let Some(fm) = front_matter(text) else { return text.into() };
if !fm.lines.iter().any(|l| l.starts_with("sealed:")) {
return text.into();
}
let mut out = String::with_capacity(text.len());
out.push_str("---\n");
for l in &fm.lines {
if !l.starts_with("sealed:") {
out.push_str(l);
out.push('\n');
}
}
out.push_str(after_close(text, &fm));
out.into()
}
fn after_close<'a>(text: &'a str, fm: &FrontMatter<'_>) -> &'a str {
let newline = usize::from(text[..fm.end].ends_with('\n'));
&text[fm.end - newline - 3..]
}
pub fn with_pointer(text: &str, lock_name: &str) -> Option<String> {
let text = normalise(text);
let fm = front_matter(&text)?;
let pointer = format!("{POINTER_KEY}: {lock_name}");
let mut out = String::with_capacity(text.len() + pointer.len() + 1);
out.push_str("---\n");
let mut placed = false;
for l in &fm.lines {
if key_value(l, POINTER_KEY).is_some() {
if !placed {
out.push_str(&pointer);
out.push('\n');
placed = true;
}
} else {
out.push_str(l);
out.push('\n');
}
}
if !placed {
out.push_str(&pointer);
out.push('\n');
}
out.push_str(after_close(&text, &fm));
Some(out)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Citation {
pub text: String,
pub line: u32,
#[serde(skip)]
pub id: Result<SymbolId, IdError>,
}
pub fn citations(text: &str) -> Vec<Citation> {
let text = normalise(text);
let (body, first_line) = match front_matter(&text) {
Some(fm) => (&text[fm.end..], fm.lines.len() as u32 + 3),
None => (&text[..], 1),
};
let mut out = Vec::new();
let mut fence: Option<(char, usize)> = None;
let mut para: Vec<(u32, &str)> = Vec::new();
for (i, line) in body.lines().enumerate() {
let n = first_line + i as u32;
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
match fence {
Some((c, len)) => {
let run = trimmed.chars().take_while(|&x| x == c).count();
if indent <= 3 && run >= len && trimmed[run * c.len_utf8()..].trim().is_empty() {
fence = None;
}
continue;
}
None => {
if let Some(open) = fence_open(trimmed).filter(|_| indent <= 3) {
scan_paragraph(¶, &mut out);
para.clear();
fence = Some(open);
continue;
}
}
}
if line.trim().is_empty() {
scan_paragraph(¶, &mut out);
para.clear();
} else {
para.push((n, line));
}
}
scan_paragraph(¶, &mut out);
out
}
fn fence_open(trimmed: &str) -> Option<(char, usize)> {
for c in ['`', '~'] {
let run = trimmed.chars().take_while(|&x| x == c).count();
if run >= 3 && !(c == '`' && trimmed[run..].contains('`')) {
return Some((c, run));
}
}
None
}
fn scan_paragraph(lines: &[(u32, &str)], out: &mut Vec<Citation>) {
if lines.is_empty() {
return;
}
let mut joined = String::new();
let mut starts = Vec::with_capacity(lines.len());
for (i, (n, l)) in lines.iter().enumerate() {
if i > 0 {
joined.push('\n');
}
starts.push((joined.len(), *n));
joined.push_str(l);
}
let line_at = |pos: usize| starts.iter().rev().find(|(s, _)| *s <= pos).map_or(lines[0].0, |(_, n)| *n);
let b = joined.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'\\' {
i += 2;
continue;
}
if b[i] != b'`' {
i += 1;
continue;
}
let open = run_len(b, i);
let content_start = i + open;
let mut j = content_start;
let mut close = None;
while j < b.len() {
if b[j] == b'`' {
let r = run_len(b, j);
if r == open {
close = Some(j);
break;
}
j += r;
} else {
j += 1;
}
}
match close {
Some(end) => {
let raw = joined[content_start..end].replace('\n', " ");
let content = strip_one_space(&raw);
if looks_global(content) {
out.push(Citation { text: content.to_owned(), line: line_at(i), id: SymbolId::parse(content) });
}
i = end + open;
}
None => i = content_start,
}
}
}
fn looks_global(content: &str) -> bool {
let Some(rest) = content.strip_prefix("sym:") else { return false };
let Some((manager, _)) = rest.split_once(' ') else { return false };
let mut chars = manager.chars();
manager != "extern"
&& chars.next().is_some_and(|c| c.is_ascii_lowercase())
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
fn run_len(b: &[u8], at: usize) -> usize {
b[at..].iter().take_while(|&&c| c == b'`').count()
}
fn strip_one_space(s: &str) -> &str {
if s.len() >= 2 && s.starts_with(' ') && s.ends_with(' ') && !s.bytes().all(|c| c == b' ') {
&s[1..s.len() - 1]
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pointer_removal_is_exact() {
let t = "---\nid: X-01\nsealed: seals.lock\nsealedish: no\n---\nsealed: body text stays\n";
assert_eq!(without_pointer(t), "---\nid: X-01\nsealedish: no\n---\nsealed: body text stays\n");
let t = "---\nid: X-01\nsealed: seals.lock\n---";
assert_eq!(without_pointer(t), "---\nid: X-01\n---");
}
#[test]
fn with_pointer_replaces_and_dedups() {
let t = "---\nid: X-01\nsealed: other.lock\nsealed: x\n---\nbody";
assert_eq!(with_pointer(t, "seals.lock").unwrap(), "---\nid: X-01\nsealed: seals.lock\n---\nbody");
assert_eq!(with_pointer("---\nid: X\n---", "s").unwrap(), "---\nid: X\nsealed: s\n---");
assert!(with_pointer("no front matter", "s").is_none());
}
#[test]
fn code_spans_follow_commonmark() {
let t = "a ``` sym:cargo x . `q``r`# ``` b `sym:cargo x . X#` c `not` d \\`sym:cargo x . E#`\n";
let c: Vec<_> = citations(t).into_iter().map(|c| c.text).collect();
assert_eq!(c, ["sym:cargo x . `q``r`#", "sym:cargo x . X#"]);
let c = citations("``` x\n");
assert!(c.is_empty());
assert!(citations("a ` b `sym:cargo x . Y#`").is_empty());
}
#[test]
fn fences_and_paragraphs() {
let t = "x\n~~~~\n`sym:k a . A#`\n~~~\n`sym:k a . B#`\n~~~~\n`sym:k a . C#`\n\n ```\n`sym:k a . D#`\n";
let c: Vec<_> = citations(t).into_iter().map(|c| (c.text, c.line)).collect();
assert_eq!(c, [("sym:k a . C#".to_owned(), 7), ("sym:k a . D#".to_owned(), 10)]);
assert!(citations("`sym:k a\n\n. A#`").is_empty());
}
#[test]
fn only_global_shaped_spans_are_citations() {
let t = "`sym:` `sym:extern tokio::spawn` `sym:? insert` `sym:Cargo x . A#` `sym:cargo x . A#()` `sym:npm-2 @a/b . f().`";
let c: Vec<_> = citations(t).into_iter().map(|c| (c.text, c.id.is_ok())).collect();
assert_eq!(c, [("sym:cargo x . A#()".to_owned(), false), ("sym:npm-2 @a/b . f().".to_owned(), true)]);
}
}