use std::collections::HashSet;
const MARKDOWN_EXTENSIONS: &[&str] = &["md", "markdown", "mdown", "mkd"];
pub fn is_markdown(path: &std::path::Path) -> bool {
path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
MARKDOWN_EXTENSIONS
.iter()
.any(|md| e.eq_ignore_ascii_case(md))
})
}
pub fn expands_tokens(path: &std::path::Path, kind: crate::error::ItemKind) -> bool {
is_markdown(path) || kind == crate::error::ItemKind::Workflow
}
pub fn item_lists_file(
item: &crate::catalog::CatalogItem,
root: &std::path::Path,
file: &std::path::Path,
) -> bool {
root.is_dir()
&& file
.strip_prefix(root)
.is_ok_and(|rel| item.expand.iter().any(|e| std::path::Path::new(e) == rel))
}
pub fn item_expands_tokens(
item: &crate::catalog::CatalogItem,
root: &std::path::Path,
file: &std::path::Path,
) -> bool {
let source_like: &std::path::Path = if root.is_dir() { file } else { &item.path };
expands_tokens(source_like, item.kind) || item_lists_file(item, root, file)
}
pub fn flatten_display(text: &str) -> String {
const OPEN: &str = "{{ns:";
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(pos) = rest.find(OPEN) {
out.push_str(&rest[..pos]);
let after = &rest[pos + OPEN.len()..];
let Some(end) = after.find("}}") else {
out.push_str(&rest[pos..]);
return out;
};
let name = after[..end].trim();
out.push_str(name);
rest = &after[end + 2..];
}
out.push_str(rest);
out
}
pub fn apply(bare: &str, prefix: &Option<String>) -> String {
match prefix {
Some(p) if !p.is_empty() => format!("{p}:{bare}"),
_ => bare.to_string(),
}
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '-' || c == '_'
}
pub fn templatize(content: &str, siblings: &HashSet<String>) -> (String, usize) {
let doc = Structure::new(content);
let mut out = String::with_capacity(content.len());
let mut count = 0usize;
let mut word: Option<usize> = None;
let mut before: Option<char> = None;
let mut i = 0usize;
while i < content.len() {
let rest = &content[i..];
if rest.starts_with("{{") {
let Some(close) = rest.find("}}") else {
count += emit_word(
content,
word.take(),
i,
&doc,
siblings,
before,
None,
&mut out,
);
out.push_str(rest);
return (out, count);
};
count += emit_word(
content,
word.take(),
i,
&doc,
siblings,
before,
None,
&mut out,
);
out.push_str(&rest[..close + 2]);
i += close + 2;
before = Some('}');
continue;
}
let c = rest.chars().next().expect("non-empty remainder");
if is_word_char(c) {
word.get_or_insert(i);
i += c.len_utf8();
continue;
}
count += emit_word(
content,
word.take(),
i,
&doc,
siblings,
before,
Some(c),
&mut out,
);
out.push(c);
before = Some(c);
i += c.len_utf8();
}
count += emit_word(
content,
word.take(),
content.len(),
&doc,
siblings,
before,
None,
&mut out,
);
(out, count)
}
#[allow(clippy::too_many_arguments)]
fn emit_word(
content: &str,
start: Option<usize>,
end: usize,
doc: &Structure,
siblings: &HashSet<String>,
before: Option<char>,
after: Option<char>,
out: &mut String,
) -> usize {
let Some(start) = start else { return 0 };
let word = &content[start..end];
if is_wrappable_mention(doc, start, word, siblings, before, after) {
out.push_str("{{ns:");
out.push_str(word);
out.push_str("}}");
1
} else {
out.push_str(word);
0
}
}
fn is_wrappable_mention(
doc: &Structure,
start: usize,
word: &str,
siblings: &HashSet<String>,
before: Option<char>,
after: Option<char>,
) -> bool {
let path_adj = matches!(before, Some('/') | Some('~')) || matches!(after, Some('/'));
!path_adj
&& matches!(doc.at(start), Cell::Prose | Cell::FmDescription)
&& siblings.contains(word)
}
pub fn prefix_choice(answer: &str) -> Option<String> {
let a = answer.trim();
match a.to_ascii_lowercase().as_str() {
"" | "y" | "yes" => None,
"n" | "no" | "none" => Some(String::new()),
_ => Some(a.to_string()),
}
}
const EXTRA_RESERVED: &[&str] = &[
"command",
"workflow",
"hook",
"mcp",
"plugin",
"prompt",
"mode",
"output-style",
];
pub(crate) fn is_safe_prefix_component(prefix: &str) -> bool {
if prefix.is_empty() || prefix == "." || prefix == ".." {
return false;
}
if prefix.starts_with('~') {
return false;
}
for b in prefix.bytes() {
if b < 0x20 || b == 0x7f {
return false;
}
if b == b'/' || b == b'\\' || b == b':' || b == b'\0' {
return false;
}
}
if crate::sanitize::has_blocked_chars(prefix) {
return false;
}
let mut comps = std::path::Path::new(prefix).components();
matches!(comps.next(), Some(std::path::Component::Normal(_))) && comps.next().is_none()
}
pub fn validate_prefix(prefix: &str) -> crate::error::Result<()> {
if prefix.is_empty() {
return Ok(());
}
if !is_safe_prefix_component(prefix) {
return Err(crate::error::MindError::UnsafePrefix {
prefix: prefix.to_string(),
});
}
if crate::error::ItemKind::parse(prefix).is_some() {
return Err(crate::error::MindError::ReservedPrefix {
prefix: prefix.to_string(),
});
}
if EXTRA_RESERVED.contains(&prefix) {
return Err(crate::error::MindError::ReservedPrefix {
prefix: prefix.to_string(),
});
}
Ok(())
}
pub(crate) fn bare_agent_names<'a>(
siblings: impl IntoIterator<Item = (crate::error::ItemKind, &'a str)>,
) -> HashSet<String> {
let mut agents: HashSet<String> = HashSet::new();
let mut others: HashSet<String> = HashSet::new();
for (kind, name) in siblings {
if kind == crate::error::ItemKind::Agent {
agents.insert(name.to_string());
} else {
others.insert(name.to_string());
}
}
agents.retain(|n| !others.contains(n));
agents
}
pub fn expand(
content: &str,
prefix: &Option<String>,
siblings: &HashSet<String>,
bare_names: &HashSet<String>,
) -> Result<String, String> {
const OPEN: &str = "{{ns:";
let mut out = String::with_capacity(content.len());
let mut rest = content;
while let Some(pos) = rest.find(OPEN) {
out.push_str(&rest[..pos]);
let after = &rest[pos + OPEN.len()..];
let Some(end) = after.find("}}") else {
out.push_str(&rest[pos..]);
return Ok(out);
};
let name = after[..end].trim();
if !siblings.contains(name) {
return Err(name.to_string());
}
if bare_names.contains(name) {
out.push_str(name);
} else {
out.push_str(&apply(name, prefix));
}
rest = &after[end + 2..];
}
out.push_str(rest);
Ok(out)
}
#[derive(Debug, Clone)]
pub struct PathSibling {
pub kind: crate::error::ItemKind,
pub name: String,
pub bin: Option<String>,
}
pub struct PathCtx<'a> {
pub store_root: &'a std::path::Path,
pub home: Option<&'a std::path::Path>,
pub prefix: &'a Option<String>,
pub self_kind: crate::error::ItemKind,
pub self_name: &'a str,
pub siblings: &'a [PathSibling],
}
impl PathCtx<'_> {
fn store_path(&self, kind: crate::error::ItemKind, bare: &str) -> String {
let abs = self
.store_root
.join(kind.as_str())
.join(apply(bare, self.prefix));
render_under_home(&abs, self.home)
}
}
fn render_under_home(path: &std::path::Path, home: Option<&std::path::Path>) -> String {
if let Some(home) = home
&& let Ok(rest) = path.strip_prefix(home)
{
return std::path::Path::new("~")
.join(rest)
.to_string_lossy()
.into_owned();
}
path.to_string_lossy().into_owned()
}
enum Token {
Path(String),
Passthrough,
Bad(crate::error::BadRefReason),
}
pub fn expand_paths(
content: &str,
ctx: &PathCtx,
) -> Result<String, (String, crate::error::BadRefReason)> {
let mut out = String::with_capacity(content.len());
let mut rest = content;
while let Some(pos) = rest.find("{{") {
out.push_str(&rest[..pos]);
let after = &rest[pos + 2..];
let Some(end) = after.find("}}") else {
out.push_str(&rest[pos..]);
return Ok(out);
};
let inner = after[..end].trim();
match resolve_token(inner, ctx) {
Token::Path(p) => {
out.push_str(&p);
rest = &after[end + 2..];
}
Token::Bad(reason) => {
return Err((rest[pos..pos + 2 + end + 2].to_string(), reason));
}
Token::Passthrough => {
out.push_str("{{");
rest = after;
}
}
}
out.push_str(rest);
Ok(out)
}
fn resolve_token(inner: &str, ctx: &PathCtx) -> Token {
if inner == "self" {
return Token::Path(ctx.store_path(ctx.self_kind, ctx.self_name));
}
if let Some(name) = inner.strip_prefix("tools:") {
let name = name.trim();
return match ctx
.siblings
.iter()
.find(|s| s.kind == crate::error::ItemKind::Tool && s.name == name)
{
Some(tool) => match &tool.bin {
Some(bin) => Token::Path(
std::path::Path::new(&ctx.store_path(crate::error::ItemKind::Tool, name))
.join(bin)
.to_string_lossy()
.into_owned(),
),
None => Token::Bad(crate::error::BadRefReason::ToolNoBin),
},
None => Token::Bad(crate::error::BadRefReason::NoMatch),
};
}
if let Some(reference) = inner.strip_prefix("path:") {
let reference = reference.trim();
let (want_kind, name) = match reference.split_once(':') {
Some((k, n)) => match crate::error::ItemKind::parse(k) {
Some(kind) => (Some(kind), n.trim()),
None => return Token::Bad(crate::error::BadRefReason::NoMatch),
},
None => (None, reference),
};
let mut hits = ctx
.siblings
.iter()
.filter(|s| s.name == name && want_kind.is_none_or(|k| s.kind == k));
return match (hits.next(), hits.next()) {
(Some(s), None) => Token::Path(ctx.store_path(s.kind, name)),
(Some(_), Some(_)) => Token::Bad(crate::error::BadRefReason::AmbiguousKind),
(None, _) => Token::Bad(crate::error::BadRefReason::NoMatch),
};
}
Token::Passthrough
}
const HOME_MARKERS: [&str; 5] = ["~/", "$HOME/", "${HOME}/", "/home/", "/Users/"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HardcodedKind {
OwnResource,
SharedTool,
OtherItem,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HardcodedPath {
pub matched: String,
pub suggestion: Option<String>,
pub kind: HardcodedKind,
}
fn canonical_install_path(path: &str) -> Option<String> {
let rest = if let Some(r) = path.strip_prefix("~/") {
r
} else if let Some(r) = path.strip_prefix("$HOME/") {
r
} else if let Some(r) = path.strip_prefix("${HOME}/") {
r
} else {
let r = path
.strip_prefix("/home/")
.or_else(|| path.strip_prefix("/Users/"))?;
r.split_once('/').map(|(_user, rest)| rest)?
};
if rest.starts_with(".mind/store/")
|| rest.starts_with(".claude/")
|| rest.starts_with(".agents/")
{
Some(format!("~/{rest}"))
} else {
None
}
}
fn is_path_terminator(c: char) -> bool {
c.is_whitespace()
|| matches!(
c,
'"' | '\'' | '`' | ')' | ']' | '}' | ',' | ';' | '<' | '>'
)
}
fn parse_install_path(path: &str) -> Option<(crate::error::ItemKind, String, String)> {
let after_kind = if let Some(rest) = path.strip_prefix("~/.mind/store/") {
let mut it = rest.splitn(2, '/');
let kind = crate::error::ItemKind::parse(it.next()?)?;
(kind, it.next()?.to_string())
} else {
let rest = path
.strip_prefix("~/.claude/")
.or_else(|| path.strip_prefix("~/.agents/"))?;
let mut it = rest.splitn(2, '/');
let kind = crate::error::ItemKind::from_dir(it.next()?)?;
(kind, it.next()?.to_string())
};
let (kind, tail) = after_kind;
let mut seg = tail.splitn(2, '/');
let first = seg.next()?;
let rest = seg.next().unwrap_or("").to_string();
let name = match kind {
crate::error::ItemKind::Agent
| crate::error::ItemKind::Rule
| crate::error::ItemKind::Command => first.strip_suffix(".md").unwrap_or(first).to_string(),
crate::error::ItemKind::Workflow => first.strip_suffix(".js").unwrap_or(first).to_string(),
_ => first.to_string(),
};
if name.is_empty() {
return None;
}
Some((kind, name, rest))
}
fn join_token(token: &str, rest: &str) -> String {
if rest.is_empty() {
token.to_string()
} else {
format!("{token}/{rest}")
}
}
fn token_for_path(path: &str, ctx: &PathCtx) -> Option<String> {
let (kind, name, rest) = parse_install_path(path)?;
if kind == ctx.self_kind && name == ctx.self_name {
return Some(join_token("{{self}}", &rest));
}
let sib = ctx
.siblings
.iter()
.find(|s| s.kind == kind && s.name == name)?;
if kind == crate::error::ItemKind::Tool {
if let Some(bin) = &sib.bin
&& rest == *bin
{
return Some(format!("{{{{tools:{name}}}}}"));
}
return Some(join_token(&format!("{{{{path:tool:{name}}}}}"), &rest));
}
Some(join_token(
&format!("{{{{path:{}:{}}}}}", kind.as_str(), name),
&rest,
))
}
fn classify_path(canonical: &str, ctx: &PathCtx) -> (HardcodedKind, Option<String>) {
let suggestion = token_for_path(canonical, ctx);
let kind = match parse_install_path(canonical) {
Some((k, name, _)) => {
if k == ctx.self_kind && name == ctx.self_name {
HardcodedKind::OwnResource
} else if k == crate::error::ItemKind::Tool
&& ctx.siblings.iter().any(|s| s.kind == k && s.name == name)
{
HardcodedKind::SharedTool
} else {
HardcodedKind::OtherItem
}
}
None => HardcodedKind::OtherItem,
};
(kind, suggestion)
}
fn scan_hardcoded(content: &str, ctx: &PathCtx) -> Vec<(usize, usize, HardcodedPath)> {
let mut out = Vec::new();
let mut i = 0;
while i < content.len() {
let Some((start, marker)) = HOME_MARKERS
.iter()
.filter_map(|m| content[i..].find(m).map(|off| (i + off, *m)))
.min_by_key(|(pos, _)| *pos)
else {
break;
};
let scan_from = start + marker.len();
let mut end = content.len();
for (idx, c) in content[scan_from..].char_indices() {
if is_path_terminator(c) {
end = scan_from + idx;
break;
}
}
let matched = content[start..end].to_string();
if let Some(canonical) = canonical_install_path(&matched) {
let (kind, suggestion) = classify_path(&canonical, ctx);
out.push((
start,
end,
HardcodedPath {
matched,
suggestion,
kind,
},
));
}
i = end.max(start + 1);
}
out
}
pub fn detect_hardcoded_paths(content: &str, ctx: &PathCtx) -> Vec<HardcodedPath> {
scan_hardcoded(content, ctx)
.into_iter()
.map(|(_, _, hp)| hp)
.collect()
}
pub fn rewrite_hardcoded_paths(content: &str, ctx: &PathCtx) -> (String, usize) {
let mut out = String::with_capacity(content.len());
let mut last = 0;
let mut count = 0;
for (start, end, hp) in scan_hardcoded(content, ctx) {
if let Some(token) = hp.suggestion {
out.push_str(&content[last..start]);
out.push_str(&token);
last = end;
count += 1;
}
}
out.push_str(&content[last..]);
(out, count)
}
fn scan_braced(content: &str) -> Vec<(usize, usize, &str)> {
let mut out = Vec::new();
let mut pos = 0usize;
while let Some(rel) = content[pos..].find("{{") {
let start = pos + rel;
let after_start = start + 2;
let Some(rel_end) = content[after_start..].find("}}") else {
break;
};
let inner_end = after_start + rel_end;
let end = inner_end + 2;
out.push((start, end, content[after_start..inner_end].trim()));
pos = end;
}
out
}
fn strip_braced(content: &str) -> String {
let mut out = String::with_capacity(content.len());
let mut last = 0;
for (start, end, _) in scan_braced(content) {
out.push_str(&content[last..start]);
out.push(' ');
last = end;
}
out.push_str(&content[last..]);
out
}
pub fn inert_tokens(content: &str) -> Vec<String> {
let mut seen: HashSet<&str> = HashSet::new();
let mut out: Vec<String> = Vec::new();
for (start, end, _) in scan_braced(content) {
let tok = &content[start..end];
if seen.insert(tok) {
out.push(tok.to_string());
}
}
out
}
pub fn bare_tool_refs(content: &str, siblings: &[PathSibling]) -> Vec<String> {
let stripped = strip_braced(content);
let mut found: Vec<String> = siblings
.iter()
.filter(|s| s.kind == crate::error::ItemKind::Tool)
.map(|s| s.name.clone())
.filter(|name| whole_word_present(&stripped, name))
.collect();
found.sort();
found.dedup();
found
}
pub fn referenced_names(content: &str) -> Vec<String> {
const OPEN: &str = "{{ns:";
let mut names: Vec<String> = Vec::new();
let mut rest = content;
while let Some(pos) = rest.find(OPEN) {
let after = &rest[pos + OPEN.len()..];
let Some(end) = after.find("}}") else {
break;
};
let name = after[..end].trim();
if !name.is_empty() && !names.iter().any(|n| n == name) {
names.push(name.to_string());
}
rest = &after[end + 2..];
}
names
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SiblingRef {
pub token: String,
pub name: String,
pub kind: Option<crate::error::ItemKind>,
}
pub fn sibling_reference_tokens(content: &str) -> Vec<SiblingRef> {
let mut out: Vec<SiblingRef> = Vec::new();
let mut rest = content;
while let Some(pos) = rest.find("{{") {
let after = &rest[pos + 2..];
let Some(end) = after.find("}}") else {
break;
};
let raw = &after[..end];
let inner = raw.trim();
let token = rest[pos..pos + 2 + end + 2].to_string();
let parsed: Option<(String, Option<crate::error::ItemKind>)> = if inner == "self" {
None
} else if let Some(name) = raw.strip_prefix("ns:") {
Some((name.trim().to_string(), None))
} else if let Some(name) = inner.strip_prefix("tools:") {
Some((name.trim().to_string(), Some(crate::error::ItemKind::Tool)))
} else if let Some(reference) = inner.strip_prefix("path:") {
let reference = reference.trim();
match reference.split_once(':') {
Some((k, n)) => match crate::error::ItemKind::parse(k) {
Some(kind) => Some((n.trim().to_string(), Some(kind))),
None => Some((reference.to_string(), None)),
},
None => Some((reference.to_string(), None)),
}
} else {
None
};
match parsed {
Some((name, kind)) => {
if !out.iter().any(|r| r.token == token) {
out.push(SiblingRef { token, name, kind });
}
rest = &after[end + 2..];
}
None => rest = after,
}
}
out
}
pub fn unguarded_refs(content: &str, siblings: &HashSet<String>) -> Vec<String> {
let doc = Structure::new(content);
let mut found: Vec<String> = Vec::new();
let mut word: Option<usize> = None;
let mut before: Option<char> = None;
let mut i = 0usize;
while i < content.len() {
let rest = &content[i..];
if rest.starts_with("{{") {
note_mention(
content,
word.take(),
i,
&doc,
siblings,
before,
None,
&mut found,
);
let Some(close) = rest.find("}}") else {
break;
};
i += close + 2;
before = Some('}');
continue;
}
let c = rest.chars().next().expect("non-empty remainder");
if is_word_char(c) {
word.get_or_insert(i);
i += c.len_utf8();
continue;
}
note_mention(
content,
word.take(),
i,
&doc,
siblings,
before,
Some(c),
&mut found,
);
before = Some(c);
i += c.len_utf8();
}
note_mention(
content,
word.take(),
content.len(),
&doc,
siblings,
before,
None,
&mut found,
);
found.sort();
found.dedup();
found
}
#[allow(clippy::too_many_arguments)]
fn note_mention(
content: &str,
start: Option<usize>,
end: usize,
doc: &Structure,
siblings: &HashSet<String>,
before: Option<char>,
after: Option<char>,
found: &mut Vec<String>,
) {
let Some(start) = start else { return };
let word = &content[start..end];
if is_wrappable_mention(doc, start, word, siblings, before, after) {
found.push(word.to_string());
}
}
fn whole_word_present(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return false;
}
let mut start = 0;
while let Some(i) = haystack[start..].find(needle) {
let idx = start + i;
let before = haystack[..idx].chars().next_back();
let after = haystack[idx + needle.len()..].chars().next();
if !before.is_some_and(is_word) && !after.is_some_and(is_word) {
return true;
}
start = idx + needle.len();
}
false
}
fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NsContext {
Prose,
CodeBlock,
CodeSpan,
Path,
FrontmatterName,
}
impl NsContext {
pub fn is_misplaced(self) -> bool {
!matches!(self, NsContext::Prose)
}
}
#[derive(Debug, Clone)]
pub struct NsRef {
pub name: String,
pub context: NsContext,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Cell {
Prose,
Code,
Span,
LinkSyntax,
FmName,
FmDescription,
FmOther,
Skip,
}
struct Structure {
cells: Vec<Cell>,
}
impl Structure {
fn new(content: &str) -> Self {
let mut cells = vec![Cell::Prose; content.len()];
let body = mark_frontmatter(content, &mut cells);
let opts = pulldown_cmark::Options::ENABLE_TABLES;
let parser = pulldown_cmark::Parser::new_ext(&content[body..], opts);
let refdefs: Vec<std::ops::Range<usize>> = parser
.reference_definitions()
.iter()
.map(|(_, def)| (def.span.start + body)..(def.span.end + body))
.collect();
for range in refdefs {
fill(&mut cells, range, Cell::LinkSyntax);
}
let mut depth = 0usize;
let mut links: Vec<bool> = Vec::new();
for (event, range) in parser.into_offset_iter() {
let range = (range.start + body)..(range.end + body);
match event {
pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(_)) => {
fill(&mut cells, range, Cell::Skip);
depth += 1;
}
pulldown_cmark::Event::End(pulldown_cmark::TagEnd::CodeBlock) => {
depth = depth.saturating_sub(1);
}
pulldown_cmark::Event::Start(
pulldown_cmark::Tag::Link { link_type, .. }
| pulldown_cmark::Tag::Image { link_type, .. },
) => {
fill(&mut cells, range, Cell::LinkSyntax);
links.push(text_doubles_as_syntax(link_type));
}
pulldown_cmark::Event::End(
pulldown_cmark::TagEnd::Link | pulldown_cmark::TagEnd::Image,
) => {
links.pop();
}
pulldown_cmark::Event::Text(_) if depth > 0 => {
fill(&mut cells, range, Cell::Code);
}
pulldown_cmark::Event::Text(_)
if !links.is_empty() && links.iter().all(|opaque| !opaque) =>
{
fill(&mut cells, range, Cell::Prose);
}
pulldown_cmark::Event::Code(_) => fill(&mut cells, range, Cell::Span),
_ => {}
}
}
Structure { cells }
}
fn at(&self, pos: usize) -> Cell {
self.cells.get(pos).copied().unwrap_or(Cell::Prose)
}
fn context(&self, content: &str, start: usize, end: usize) -> Option<NsContext> {
Some(match self.at(start) {
Cell::Skip => return None,
Cell::FmName => NsContext::FrontmatterName,
Cell::FmDescription | Cell::FmOther => NsContext::Prose,
Cell::Code => NsContext::CodeBlock,
Cell::Span => NsContext::CodeSpan,
Cell::LinkSyntax => NsContext::Path,
Cell::Prose if path_adjacent(content, start, end) => NsContext::Path,
Cell::Prose => NsContext::Prose,
})
}
}
fn text_doubles_as_syntax(link_type: pulldown_cmark::LinkType) -> bool {
use pulldown_cmark::LinkType as L;
matches!(
link_type,
L::Shortcut
| L::ShortcutUnknown
| L::Collapsed
| L::CollapsedUnknown
| L::Autolink
| L::Email
| L::WikiLink { .. }
)
}
fn fill(cells: &mut [Cell], range: std::ops::Range<usize>, cell: Cell) {
let end = range.end.min(cells.len());
if range.start < end {
cells[range.start..end].fill(cell);
}
}
fn mark_frontmatter(content: &str, cells: &mut [Cell]) -> usize {
const DESCRIPTION_KEY: &str = "description:";
let bom = if content.starts_with('\u{feff}') {
'\u{feff}'.len_utf8()
} else {
0
};
fill(cells, 0..bom, Cell::Skip);
let mut lines = content[bom..].split_inclusive('\n');
let Some(first) = lines.next() else {
return bom;
};
if first.trim() != "---" {
return bom;
}
fill(cells, 0..bom + first.len(), Cell::Skip);
let mut offset = bom + first.len();
for raw in lines {
let line = raw.strip_suffix('\n').unwrap_or(raw);
let end = offset + raw.len();
if line.trim() == "---" {
fill(cells, offset..end, Cell::Skip);
return end;
}
let trimmed = line.trim_start();
if trimmed.starts_with("name:") {
fill(cells, offset..end, Cell::FmName);
} else if trimmed.starts_with(DESCRIPTION_KEY) {
let indent = line.len() - trimmed.len();
let value_start = offset + indent + DESCRIPTION_KEY.len();
fill(cells, offset..value_start, Cell::FmOther);
fill(cells, value_start..end, Cell::FmDescription);
} else {
fill(cells, offset..end, Cell::FmOther);
}
offset = end;
}
content.len()
}
fn path_adjacent(content: &str, start: usize, end: usize) -> bool {
let before = content[..start].chars().next_back();
let after = content[end..].chars().next();
matches!(before, Some('/') | Some('~')) || matches!(after, Some('/'))
}
pub fn scan_ns_refs(content: &str) -> Vec<NsRef> {
const OPEN: &str = "{{ns:";
let mut out = Vec::new();
let doc = Structure::new(content);
let mut from = 0usize;
while let Some(rel) = content[from..].find(OPEN) {
let start = from + rel;
let after = &content[start + OPEN.len()..];
let Some(erel) = after.find("}}") else { break };
let end = start + OPEN.len() + erel + 2;
let name = after[..erel].trim();
if !name.is_empty()
&& let Some(context) = doc.context(content, start, end)
{
out.push(NsRef {
name: name.to_string(),
context,
start,
end,
});
}
from = end;
}
out
}
pub fn unwrap_misplaced(content: &str, all_code: bool) -> (String, usize) {
let mut out = String::with_capacity(content.len());
let mut last = 0;
let mut count = 0;
for r in scan_ns_refs(content) {
if all_code || r.context.is_misplaced() {
out.push_str(&content[last..r.start]);
out.push_str(&r.name);
last = r.end;
count += 1;
}
}
out.push_str(&content[last..]);
(out, count)
}
#[cfg(test)]
mod tests {
use super::*;
fn sibs(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn bare_agent_names_excludes_names_shadowed_by_non_agents() {
use crate::error::ItemKind;
let got = bare_agent_names([
(ItemKind::Agent, "a"),
(ItemKind::Agent, "b"),
(ItemKind::Skill, "b"),
(ItemKind::Rule, "c"),
]);
assert_eq!(got, sibs(&["a"]));
assert!(bare_agent_names(std::iter::empty()).is_empty());
}
#[test]
fn apply_prefixes_or_passes_through() {
assert_eq!(apply("review", &Some("jk".into())), "jk:review");
assert_eq!(apply("review", &None), "review");
assert_eq!(apply("review", &Some(String::new())), "review");
}
#[test]
fn is_markdown_matches_every_recognized_extension_case_insensitively() {
for name in [
"SKILL.md",
"notes.markdown",
"notes.MARKDOWN",
"notes.mdown",
"notes.mkd",
"notes.Md",
] {
assert!(
is_markdown(std::path::Path::new(name)),
"{name} should be recognized as markdown"
);
}
for name in [
"run.sh",
"TOOL",
"lib.py",
"notes.txt",
"notes",
"notes.mdx",
"run.md.sh",
"notes.md.bak",
".md",
".markdown",
"docs.md/run.sh",
] {
assert!(
!is_markdown(std::path::Path::new(name)),
"{name} should not be recognized as markdown"
);
}
}
#[test]
fn expands_tokens_grants_a_workflow_on_its_kind_not_its_extension() {
use crate::error::ItemKind;
let js = std::path::Path::new("review.js");
assert!(
expands_tokens(js, ItemKind::Workflow),
"a workflow's own file expands whatever its extension"
);
assert!(expands_tokens(
std::path::Path::new("review"),
ItemKind::Workflow
));
for kind in [
ItemKind::Skill,
ItemKind::Agent,
ItemKind::Rule,
ItemKind::Command,
ItemKind::Tool,
] {
assert!(
!expands_tokens(js, kind),
"{kind:?} must not expand tokens in a .js file"
);
assert!(
expands_tokens(std::path::Path::new("SKILL.md"), kind),
"{kind:?} still expands markdown"
);
}
}
#[test]
fn workflow_is_a_reserved_prefix() {
assert!(EXTRA_RESERVED.contains(&"workflow"));
assert!(
validate_prefix("workflow").is_err(),
"a source may not take `workflow` as its namespace prefix"
);
assert!(validate_prefix("workflows").is_ok());
}
#[test]
fn parse_install_path_strips_a_workflow_js_extension() {
use crate::error::ItemKind;
assert_eq!(
parse_install_path("~/.claude/workflows/review.js"),
Some((ItemKind::Workflow, "review".to_string(), String::new()))
);
assert_eq!(
parse_install_path("~/.mind/store/workflow/review"),
Some((ItemKind::Workflow, "review".to_string(), String::new()))
);
assert_eq!(
parse_install_path("~/.claude/workflows/jk:review.js"),
Some((ItemKind::Workflow, "jk:review".to_string(), String::new()))
);
}
#[test]
fn flatten_display_renders_ns_tokens_as_their_bare_name() {
assert_eq!(
flatten_display("hand off to {{ns:dev}} when done"),
"hand off to dev when done"
);
assert_eq!(flatten_display("see {{ns: dev }}"), "see dev");
assert_eq!(flatten_display("plain text"), "plain text");
assert_eq!(flatten_display("see {{ns:dev"), "see {{ns:dev");
assert_eq!(flatten_display("path is {{self}}"), "path is {{self}}");
}
#[test]
fn templatize_wraps_bare_siblings_and_skips_tokens() {
let s = sibs(&["dev", "style"]);
let (out, n) = templatize("hand off to dev, see {{ns:style}}, not develop", &s);
assert_eq!(
out, "hand off to {{ns:dev}}, see {{ns:style}}, not develop",
"bare `dev` is wrapped; the token and the longer word `develop` are left alone"
);
assert_eq!(n, 1, "only the one bare sibling mention is rewritten");
let (again, m) = templatize(&out, &s);
assert_eq!(again, out);
assert_eq!(m, 0);
}
#[test]
fn prefix_choice_interprets_the_meld_prompt() {
assert_eq!(prefix_choice(""), None);
assert_eq!(prefix_choice("y"), None);
assert_eq!(prefix_choice("YES"), None);
assert_eq!(prefix_choice("n"), Some(String::new()));
assert_eq!(prefix_choice("none"), Some(String::new()));
assert_eq!(prefix_choice(" MyPfx "), Some("MyPfx".to_string()));
}
#[test]
fn expand_unprefixed_yields_bare_names() {
let s = sibs(&["test"]);
let no_bare: HashSet<String> = HashSet::new();
let got = expand("hand off to {{ns:test}} now", &None, &s, &no_bare).unwrap();
assert_eq!(got, "hand off to test now");
}
#[test]
fn expand_prefixed_yields_prefixed_names() {
let s = sibs(&["test"]);
let no_bare: HashSet<String> = HashSet::new();
let got = expand("see {{ns:test}}.", &Some("jk".into()), &s, &no_bare).unwrap();
assert_eq!(got, "see jk:test.");
}
#[test]
fn expand_rejects_unknown_referent() {
let s = sibs(&["test"]);
let no_bare: HashSet<String> = HashSet::new();
assert_eq!(
expand("{{ns:nope}}", &None, &s, &no_bare),
Err("nope".to_string())
);
}
#[test]
fn expand_passes_content_without_tokens() {
let s = sibs(&["test"]);
let no_bare: HashSet<String> = HashSet::new();
assert_eq!(
expand("no tokens here", &None, &s, &no_bare).unwrap(),
"no tokens here"
);
}
#[test]
fn expand_trims_token_and_leaves_unterminated_verbatim() {
let s = sibs(&["dev"]);
let no_bare: HashSet<String> = HashSet::new();
assert_eq!(
expand("{{ns: dev }}", &Some("jk".into()), &s, &no_bare).unwrap(),
"jk:dev"
);
assert_eq!(
expand("see {{ns:dev", &None, &s, &no_bare).unwrap(),
"see {{ns:dev"
);
}
#[test]
fn expand_agent_referent_expands_bare_under_prefix() {
let s = sibs(&["dev", "review"]);
let bare = sibs(&["dev"]); let pfx = Some("jk".to_string());
assert_eq!(
expand("delegate to {{ns:dev}}", &pfx, &s, &bare).unwrap(),
"delegate to dev"
);
assert_eq!(
expand("use {{ns:review}}", &pfx, &s, &bare).unwrap(),
"use jk:review"
);
let s2 = sibs(&["shared"]);
let no_bare: HashSet<String> = HashSet::new(); assert_eq!(
expand("{{ns:shared}}", &pfx, &s2, &no_bare).unwrap(),
"jk:shared"
);
let bare2 = sibs(&["dev"]);
let s3 = sibs(&["dev"]);
assert_eq!(
expand("{{ns:ghost}}", &pfx, &s3, &bare2),
Err("ghost".to_string())
);
}
#[test]
fn unguarded_finds_bare_prose_refs_only() {
let s = sibs(&["test", "planner"]);
let refs = unguarded_refs("run the test, then {{ns:planner}}", &s);
assert_eq!(refs, vec!["test".to_string()]);
}
#[test]
fn unguarded_respects_word_boundaries() {
let s = sibs(&["do"]);
assert!(unguarded_refs("doing work", &s).is_empty());
assert_eq!(unguarded_refs("just do it", &s), vec!["do".to_string()]);
}
#[test]
fn unguarded_does_not_flag_names_already_inside_any_token() {
let s = sibs(&["detect", "other", "planner"]);
assert!(
unguarded_refs("run {{tools:detect}} to start", &s).is_empty(),
"{{tools:detect}} must not produce an unguarded-reference advisory"
);
assert!(
unguarded_refs("see {{path:skill:other}} for details", &s).is_empty(),
"{{path:skill:other}} must not produce an unguarded-reference advisory"
);
assert!(
unguarded_refs("hand off to {{ns:planner}}", &s).is_empty(),
"{{ns:planner}} must not produce an unguarded-reference advisory"
);
let bare = unguarded_refs("run detect and see other", &s);
assert_eq!(
bare,
vec!["detect".to_string(), "other".to_string()],
"bare prose sibling names must still be flagged"
);
let mixed = unguarded_refs("use {{tools:detect}} then call other directly", &s);
assert_eq!(
mixed,
vec!["other".to_string()],
"only the bare mention should be flagged when the same name also appears in a token"
);
}
#[test]
fn referenced_names_extracts_tokens_in_order_deduped() {
let got = referenced_names("see {{ns:test}} then {{ns:do}} then {{ns:test}}");
assert_eq!(got, vec!["test".to_string(), "do".to_string()]);
}
#[test]
fn referenced_names_trims_whitespace_inside_token() {
let got = referenced_names("{{ns: dev }}");
assert_eq!(got, vec!["dev".to_string()]);
}
#[test]
fn referenced_names_no_tokens_is_empty() {
assert!(referenced_names("plain prose, no tokens").is_empty());
}
#[test]
fn referenced_names_unterminated_token_is_not_a_reference() {
assert!(referenced_names("see {{ns:dev").is_empty());
assert_eq!(
referenced_names("{{ns:test}} then {{ns:dev"),
vec!["test".to_string()]
);
}
#[test]
fn referenced_names_empty_token_is_skipped() {
assert!(referenced_names("{{ns:}}").is_empty());
assert!(referenced_names("{{ns: }}").is_empty());
assert_eq!(
referenced_names("{{ns:}} then {{ns:dev}}"),
vec!["dev".to_string()]
);
}
#[test]
fn referenced_names_valid_then_unterminated_returns_valid_only() {
assert_eq!(
referenced_names("use {{ns:dev}} and then {{ns:planner"),
vec!["dev".to_string()]
);
}
#[test]
fn referenced_names_whitespace_or_empty_content_is_empty() {
assert!(referenced_names("").is_empty());
assert!(referenced_names(" \n\t ").is_empty());
}
#[test]
fn referenced_names_empty_token_does_not_swallow_following_close() {
assert_eq!(
referenced_names("{{ns:}}{{ns:dev}}"),
vec!["dev".to_string()]
);
}
#[test]
fn validate_prefix_rejects_reserved_kind_words() {
for word in ["skill", "agent", "rule", "command", "tool"] {
let err = validate_prefix(word).unwrap_err();
assert!(
matches!(err, crate::error::MindError::ReservedPrefix { ref prefix } if prefix == word),
"expected ReservedPrefix for '{word}', got: {err:?}"
);
}
}
#[test]
fn validate_prefix_accepts_normal_prefix_and_empty() {
assert!(validate_prefix("jk").is_ok(), "'jk' must be accepted");
assert!(
validate_prefix("my-org").is_ok(),
"'my-org' must be accepted"
);
assert!(validate_prefix("").is_ok(), "empty must be accepted");
}
#[test]
fn validate_prefix_rejects_path_traversal_and_separators() {
for bad in [
"../evil", "..", ".", "a/b", "a\\b", "~home", "a:b", "a\x00b",
] {
let err = validate_prefix(bad).unwrap_err();
assert!(
matches!(err, crate::error::MindError::UnsafePrefix { .. }),
"expected UnsafePrefix for {bad:?}, got {err:?}"
);
}
}
#[test]
fn validate_prefix_rejects_blocked_unicode_with_unsafe_prefix() {
for bad in [
"pay\u{202E}oot", "acme\u{E0041}", "acme\u{FE0F}", ] {
let err = validate_prefix(bad).unwrap_err();
assert!(
matches!(err, crate::error::MindError::UnsafePrefix { .. }),
"expected UnsafePrefix for {bad:?}, got {err:?}"
);
}
}
#[test]
fn is_safe_prefix_component_rejects_multibyte_control_and_bidi() {
for ok in ["plugin", "my-plugin", "caf\u{00e9}"] {
assert!(is_safe_prefix_component(ok), "{ok:?} should be accepted");
}
for bad in [
"pay\u{202E}", "a\u{2066}b", "a\u{200B}b", "a\u{200E}b", "a\u{FEFF}b", "a\u{0085}b", "acme\u{E0041}", "acme\u{FE0F}", ] {
assert!(!is_safe_prefix_component(bad), "{bad:?} should be rejected");
}
}
#[test]
fn validate_prefix_rejects_extended_reserved_words() {
for word in [
"command",
"hook",
"mcp",
"plugin",
"prompt",
"mode",
"output-style",
] {
let err = validate_prefix(word).unwrap_err();
assert!(
matches!(err, crate::error::MindError::ReservedPrefix { .. }),
"expected ReservedPrefix for {word:?}, got {err:?}"
);
}
}
use crate::error::ItemKind;
use std::path::Path;
fn psib(kind: ItemKind, name: &str, bin: Option<&str>) -> PathSibling {
PathSibling {
kind,
name: name.to_string(),
bin: bin.map(|s| s.to_string()),
}
}
fn ctx<'a>(
store: &'a Path,
prefix: &'a Option<String>,
self_kind: ItemKind,
self_name: &'a str,
siblings: &'a [PathSibling],
) -> PathCtx<'a> {
PathCtx {
store_root: store,
home: None,
prefix,
self_kind,
self_name,
siblings,
}
}
#[test]
fn self_token_resolves_to_own_store_dir() {
let store = Path::new("/m/store");
let none = None;
let c = ctx(store, &none, ItemKind::Skill, "review", &[]);
assert_eq!(
expand_paths("run {{self}}/resources/pr.py here", &c).unwrap(),
"run /m/store/skill/review/resources/pr.py here"
);
}
#[test]
fn self_token_is_prefix_aware() {
let store = Path::new("/m/store");
let pfx = Some("jk".to_string());
let c = ctx(store, &pfx, ItemKind::Skill, "review", &[]);
assert_eq!(
expand_paths("{{self}}", &c).unwrap(),
"/m/store/skill/jk:review"
);
}
#[test]
fn store_paths_render_with_tilde_when_under_home() {
let home = Path::new("/home/jk");
let store = Path::new("/home/jk/.mind/store");
let none = None;
let sibs = vec![psib(ItemKind::Tool, "shard-plan", Some("shard-plan"))];
let c = PathCtx {
store_root: store,
home: Some(home),
prefix: &none,
self_kind: ItemKind::Skill,
self_name: "review",
siblings: &sibs,
};
assert_eq!(
expand_paths("{{self}}/resources/pr.py", &c).unwrap(),
"~/.mind/store/skill/review/resources/pr.py"
);
assert_eq!(
expand_paths("{{tools:shard-plan}}", &c).unwrap(),
"~/.mind/store/tool/shard-plan/shard-plan"
);
assert_eq!(
expand_paths("{{path:tool:shard-plan}}/lib.sh", &c).unwrap(),
"~/.mind/store/tool/shard-plan/lib.sh"
);
}
#[test]
fn store_paths_stay_absolute_when_store_not_under_home() {
let home = Path::new("/home/jk");
let store = Path::new("/srv/mind/store");
let none = None;
let c = PathCtx {
store_root: store,
home: Some(home),
prefix: &none,
self_kind: ItemKind::Skill,
self_name: "review",
siblings: &[],
};
assert_eq!(
expand_paths("{{self}}", &c).unwrap(),
"/srv/mind/store/skill/review"
);
let c = PathCtx { home: None, ..c };
assert_eq!(
expand_paths("{{self}}", &c).unwrap(),
"/srv/mind/store/skill/review"
);
}
#[test]
fn tools_token_resolves_to_entrypoint() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![psib(ItemKind::Tool, "shard-plan", Some("shard-plan"))];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
assert_eq!(
expand_paths("pipe to {{tools:shard-plan}} --max 5", &c).unwrap(),
"pipe to /m/store/tool/shard-plan/shard-plan --max 5"
);
}
#[test]
fn tools_token_is_prefix_aware() {
let store = Path::new("/m/store");
let pfx = Some("jk".to_string());
let sibs = vec![psib(ItemKind::Tool, "shard-plan", Some("shard-plan"))];
let c = ctx(store, &pfx, ItemKind::Skill, "review", &sibs);
assert_eq!(
expand_paths("{{tools:shard-plan}}", &c).unwrap(),
"/m/store/tool/jk:shard-plan/shard-plan"
);
}
#[test]
fn tools_token_errors_on_missing_or_binless_or_non_tool() {
use crate::error::BadRefReason::{NoMatch, ToolNoBin};
let store = Path::new("/m/store");
let none = None;
let c = ctx(store, &none, ItemKind::Skill, "review", &[]);
assert_eq!(
expand_paths("{{tools:nope}}", &c),
Err(("{{tools:nope}}".to_string(), NoMatch))
);
let binless = vec![psib(ItemKind::Tool, "x", None)];
let c = ctx(store, &none, ItemKind::Skill, "review", &binless);
assert_eq!(
expand_paths("{{tools:x}}", &c),
Err(("{{tools:x}}".to_string(), ToolNoBin))
);
let not_tool = vec![psib(ItemKind::Skill, "x", None)];
let c = ctx(store, &none, ItemKind::Skill, "review", ¬_tool);
assert_eq!(
expand_paths("{{tools:x}}", &c),
Err(("{{tools:x}}".to_string(), NoMatch))
);
}
#[test]
fn path_token_resolves_sibling_dir_qualified_and_bare() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![psib(ItemKind::Tool, "detect", Some("detect"))];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
assert_eq!(
expand_paths("{{path:tool:detect}}/lib/helper.sh", &c).unwrap(),
"/m/store/tool/detect/lib/helper.sh"
);
assert_eq!(
expand_paths("{{path:detect}}", &c).unwrap(),
"/m/store/tool/detect"
);
}
#[test]
fn path_token_ambiguity_errors_unless_kind_qualified() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![
psib(ItemKind::Skill, "x", None),
psib(ItemKind::Agent, "x", None),
];
let c = ctx(store, &none, ItemKind::Skill, "self", &sibs);
assert_eq!(
expand_paths("{{path:x}}", &c),
Err((
"{{path:x}}".to_string(),
crate::error::BadRefReason::AmbiguousKind
))
);
assert_eq!(
expand_paths("{{path:agent:x}}", &c).unwrap(),
"/m/store/agent/x"
);
assert_eq!(
expand_paths("{{path:none}}", &c),
Err((
"{{path:none}}".to_string(),
crate::error::BadRefReason::NoMatch
))
);
}
#[test]
fn rewrite_maps_hardcoded_paths_to_tokens() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![
psib(ItemKind::Tool, "detect", Some("detect")),
psib(ItemKind::Skill, "release", None),
];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
let input = "self ~/.claude/skills/review/resources/pr.py \
tool ~/.mind/store/tool/detect/detect \
other ~/.mind/store/skill/release/x.sh \
foreign ~/.claude/skills/unknown/y.sh";
let (out, n) = rewrite_hardcoded_paths(input, &c);
assert_eq!(n, 3, "three confident rewrites: {out}");
assert!(out.contains("self {{self}}/resources/pr.py"), "{out}");
assert!(out.contains("tool {{tools:detect}}"), "{out}");
assert!(out.contains("other {{path:skill:release}}/x.sh"), "{out}");
assert!(
out.contains("foreign ~/.claude/skills/unknown/y.sh"),
"{out}"
);
}
#[test]
fn detect_reports_paths_with_and_without_suggestions() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![psib(ItemKind::Tool, "detect", Some("detect"))];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
let found = detect_hardcoded_paths(
"a ~/.mind/store/tool/detect/detect b ~/.agents/resources/x.sh",
&c,
);
assert_eq!(found.len(), 2);
assert_eq!(found[0].suggestion.as_deref(), Some("{{tools:detect}}"));
assert_eq!(found[1].suggestion, None);
}
#[test]
fn hardcoded_detects_env_and_absolute_home_forms() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![psib(ItemKind::Tool, "detect", Some("detect"))];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
for path in [
"$HOME/.mind/store/tool/detect/detect",
"${HOME}/.mind/store/tool/detect/detect",
"/home/jk/.mind/store/tool/detect/detect",
"/Users/jk/.mind/store/tool/detect/detect",
] {
let found = detect_hardcoded_paths(&format!("run {path} now"), &c);
assert_eq!(found.len(), 1, "{path}");
assert_eq!(found[0].matched, path, "matched span is the original form");
assert_eq!(
found[0].suggestion.as_deref(),
Some("{{tools:detect}}"),
"{path}"
);
}
assert!(detect_hardcoded_paths("see /home/jk/projects/x", &c).is_empty());
}
#[test]
fn hardcoded_classifies_own_tool_and_other() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![
psib(ItemKind::Tool, "detect", Some("detect")),
psib(ItemKind::Skill, "release", None),
];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
let found = detect_hardcoded_paths(
"own ~/.claude/skills/review/resources/pr.py \
tool ~/.mind/store/tool/detect/detect \
other ~/.mind/store/skill/release/x.sh \
foreign ~/.claude/skills/unknown/y.sh",
&c,
);
assert_eq!(found.len(), 4);
assert_eq!(found[0].kind, HardcodedKind::OwnResource);
assert_eq!(found[1].kind, HardcodedKind::SharedTool);
assert_eq!(found[2].kind, HardcodedKind::OtherItem);
assert_eq!(found[3].kind, HardcodedKind::OtherItem);
assert_eq!(found[3].suggestion, None);
}
#[test]
fn parse_install_path_strips_md_suffix_for_agent_rule_and_command() {
for (kind, dir) in [
(ItemKind::Agent, "agents"),
(ItemKind::Rule, "rules"),
(ItemKind::Command, "commands"),
] {
let lobe = format!("~/.claude/{dir}/deploy.md");
assert_eq!(
parse_install_path(&lobe),
Some((kind, "deploy".to_string(), String::new())),
"lobe spelling for {kind:?}"
);
let store = format!("~/.mind/store/{}/deploy", kind.as_str());
assert_eq!(
parse_install_path(&store),
Some((kind, "deploy".to_string(), String::new())),
"store spelling for {kind:?}"
);
}
}
#[test]
fn token_for_path_maps_command_lobe_and_store_spellings() {
let store = Path::new("/m/store");
let none = None;
let sibs = vec![psib(ItemKind::Command, "deploy", None)];
let c = ctx(store, &none, ItemKind::Skill, "review", &sibs);
let (out, n) = rewrite_hardcoded_paths(
"lobe ~/.claude/commands/deploy.md store ~/.mind/store/command/deploy",
&c,
);
assert_eq!(n, 2, "{out}");
assert!(out.contains("lobe {{path:command:deploy}}"), "{out}");
assert!(out.contains("store {{path:command:deploy}}"), "{out}");
}
#[test]
fn bare_tool_refs_finds_tool_names_outside_tokens() {
let sibs = vec![
psib(ItemKind::Tool, "detect", Some("detect")),
psib(ItemKind::Skill, "review", None),
];
let refs = bare_tool_refs("run detect then review; later {{tools:detect}}", &sibs);
assert_eq!(refs, vec!["detect".to_string()]);
assert!(bare_tool_refs("just detect", &sibs).contains(&"detect".to_string()));
}
#[test]
fn path_tokens_ignore_ns_and_handle_edges() {
let store = Path::new("/m/store");
let none = None;
let c = ctx(store, &none, ItemKind::Tool, "t", &[]);
assert_eq!(
expand_paths("{{ns:foo}} then {{self}}", &c).unwrap(),
"{{ns:foo}} then /m/store/tool/t"
);
assert_eq!(expand_paths("{{ self }}", &c).unwrap(), "/m/store/tool/t");
assert_eq!(
expand_paths("see {{self", &c).unwrap(),
"see {{self".to_string()
);
assert_eq!(expand_paths("plain prose", &c).unwrap(), "plain prose");
assert_eq!(expand_paths("a {{x}} b", &c).unwrap(), "a {{x}} b");
}
#[test]
fn scan_ns_refs_classifies_context() {
let doc = "---\nname: {{ns:dev}}\ndescription: see {{ns:review}}\n---\n\
prose {{ns:dev}} here\n`{{ns:test}}` span\n~/{{ns:dev}}\n\
```\n{{ns:do}}\n```\n";
let got: Vec<(String, NsContext)> = scan_ns_refs(doc)
.into_iter()
.map(|r| (r.name, r.context))
.collect();
assert_eq!(
got,
vec![
("dev".into(), NsContext::FrontmatterName),
("review".into(), NsContext::Prose), ("dev".into(), NsContext::Prose),
("test".into(), NsContext::CodeSpan),
("dev".into(), NsContext::Path),
("do".into(), NsContext::CodeBlock),
]
);
}
fn contexts(doc: &str) -> Vec<(String, NsContext)> {
scan_ns_refs(doc)
.into_iter()
.map(|r| (r.name, r.context))
.collect()
}
#[test]
fn token_after_a_multiline_code_span_is_prose() {
let doc = "Set up the token with `gh auth\n\
token --hostname github.com` and the bundle (via {{ns:cert}}).\n";
assert_eq!(contexts(doc), vec![("cert".into(), NsContext::Prose)]);
}
#[test]
fn token_inside_a_multiline_code_span_is_a_code_span() {
let doc = "run `mind learn\n{{ns:dev}}` now\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
}
#[test]
fn code_spans_match_by_run_length_and_stop_at_a_blank_line() {
let doc = "``a ` {{ns:dev}}`` and {{ns:do}} after\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeSpan),
("do".into(), NsContext::Prose),
]
);
let doc = "a stray ` backtick\n\nsee {{ns:dev}} here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "- a stray ` backtick\n- hand off to {{ns:dev}} ` now\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "- run `gh auth\n token` then {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "- run `gh {{ns:dev}}\n token` now\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
}
#[test]
fn fences_close_only_on_an_equal_or_longer_run_of_their_own_char() {
let doc = "````markdown\n{{ns:dev}}\n```sh\necho {{ns:do}}\n```\n````\n\
then see {{ns:dev}} in prose\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::CodeBlock),
("dev".into(), NsContext::Prose),
]
);
let doc = "~~~\n{{ns:dev}}\n```\n{{ns:do}}\n~~~\nprose {{ns:dev}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::CodeBlock),
("dev".into(), NsContext::Prose),
]
);
}
#[test]
fn a_line_initial_backtick_run_with_an_info_backtick_is_not_a_fence() {
let doc = "``run ` here`` opens nothing\n\nprose {{ns:dev}} here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
}
#[test]
fn templatize_reads_the_same_structure_as_the_token_scan() {
let s = sibs(&["dev", "do"]);
let doc = "run `gh auth\nprint` then dev runs\n\n\
````md\n```sh\ndev do\n```\n````\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "only the prose mention is wrapped: {out}");
assert!(out.contains("then {{ns:dev}} runs"), "{out}");
assert!(out.contains("dev do\n"), "fenced content untouched: {out}");
}
#[test]
fn fix_passes_never_introduce_an_unguarded_reference() {
let s = sibs(&["cos-spec", "cos-cert-setup"]);
let corpus = [
"Get it with `gh auth\ntoken --hostname github.com` (via {{ns:cos-cert-setup}}).\n",
"````md\n```sh\necho hi\n```\n````\nSee the {{ns:cos-spec}} skill for EARS.\n",
"Get it with `gh auth\ntoken` then read the cos-spec skill.\n",
"``a ` b`` is a span\n\nhand off to {{ns:cos-spec}} now\n",
];
for doc in corpus {
let (unwrapped, _) = unwrap_misplaced(doc, false);
let (fixed, _) = templatize(&unwrapped, &s);
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"--fix left an unguarded reference in: {fixed}"
);
}
}
fn fix_passes(content: &str, siblings: &HashSet<String>) -> String {
let (unwrapped, _) = unwrap_misplaced(content, false);
templatize(&unwrapped, siblings).0
}
const SHAPES: &[&str] = &[
"Read it with `gh auth\ntoken --hostname github.com` and see dev now.\n",
"````md\n```sh\ndev do\n```\n````\nThen see dev in prose.\n",
"~~~text\n```\ndev\n~~~\nThen see do.\n",
"~~~ruby\ndev\n~~~\nThen see dev.\n",
"``a ` b`` is a span\n\nhand off to dev now\n",
"- run `gh auth\n token` then dev\n- and do\n",
"| a | b |\n|---|---|\n| `dev` | do |\n",
"---\nname: dev\ndescription: see do\n---\n# Title\n\nsee do here\n",
"# Heading\n\nsee ~/dev and dev/x and dev.\n",
"```\nunclosed fence\ndev\n",
"prose {{ns:dev}} and `{{ns:do}}` and ~/{{ns:dev}}\n",
"`{{` and `}}` then dev\n",
"a\r\n\r\nsee dev here\r\n",
"> ```\n> dev\n> ```\n\nsee do after\n",
"Open it with:\n\n ```\n\nThen see dev.\n",
"This sentence wraps\n onto dev here.\n",
"1. step:\n\n ```sh\n dev do\n ```\n\nThen see dev.\n",
"Escape it with \\` in prose. Then dev and `do`.\n",
"- item:\n\n ```sh\n dev\n\nBack at top level, see do.\n",
"Setup:\n\n- ```sh\n dev\n ```\n\n Then see do.\n",
"Type a lone ` in prose\n---\nThen see dev and run `do`.\n",
"Type a lone ` in prose\n***\nThen see dev and run `do`.\n",
"Type a lone ` in prose\n<div>\nThen see dev and run `do`.\n",
"see {{ns:\ndev }} for the handoff, then do\n",
"> ```sh\n> mind learn dev\n> ```\n>\n> Then see do.\n",
];
#[test]
fn fix_is_a_fixed_point_on_every_structural_shape() {
let s = sibs(&["dev", "do"]);
for doc in SHAPES {
let once = fix_passes(doc, &s);
let twice = fix_passes(&once, &s);
assert_eq!(
once, twice,
"--fix is not idempotent on:\n{doc}\nfirst:\n{once}\nsecond:\n{twice}"
);
}
}
#[test]
fn templatize_only_ever_creates_prose_tokens() {
let s = sibs(&["dev", "do"]);
for doc in SHAPES {
let (bare, _) = unwrap_misplaced(doc, true);
let (wrapped, _) = templatize(&bare, &s);
for r in scan_ns_refs(&wrapped) {
assert!(
!r.context.is_misplaced(),
"templatize wrapped {} into {:?} in:\n{wrapped}",
r.name,
r.context
);
}
}
}
#[test]
fn wrapping_and_the_token_scan_read_one_map_position_by_position() {
let s = sibs(&["dev", "do"]);
for doc in SHAPES {
let (bare, _) = unwrap_misplaced(doc, true);
let (wrapped, _) = templatize(&bare, &s);
let wrapped_at = wrapped_offsets(&bare, &wrapped);
for name in ["dev", "do"] {
for start in occurrences(&bare, name) {
let end = start + name.len();
let did_wrap = wrapped_at.contains(&start);
let probe = format!("{}{{{{ns:{name}}}}}{}", &bare[..start], &bare[end..]);
let scanned = scan_ns_refs(&probe)
.into_iter()
.find(|r| r.start == start)
.map(|r| r.context);
let scan_prose = scanned == Some(NsContext::Prose);
if did_wrap {
assert!(
scan_prose,
"wrapping created a token at {start} the scan calls \
{scanned:?} in:\n{bare}"
);
} else {
assert!(
!scan_prose,
"the scan calls a token at {start} prose but wrapping \
declined to create one in:\n{bare}"
);
}
}
}
}
}
fn wrapped_offsets(bare: &str, wrapped: &str) -> HashSet<usize> {
let (b, w) = (bare.as_bytes(), wrapped.as_bytes());
let mut out = HashSet::new();
let (mut i, mut j) = (0usize, 0usize);
while i < b.len() {
if j < w.len() && b[i] == w[j] {
i += 1;
j += 1;
continue;
}
assert!(
wrapped[j..].starts_with("{{ns:"),
"templatize changed a byte it did not wrap:\n{bare}\n{wrapped}"
);
j += "{{ns:".len();
out.insert(i);
while let Some(c) = bare[i..].chars().next() {
if !is_word_char(c) {
break;
}
assert_eq!(&bare[i..i + c.len_utf8()], &wrapped[j..j + c.len_utf8()]);
i += c.len_utf8();
j += c.len_utf8();
}
assert!(
wrapped[j..].starts_with("}}"),
"an unterminated wrapper:\n{bare}\n{wrapped}"
);
j += 2;
}
assert_eq!(j, w.len(), "trailing bytes:\n{bare}\n{wrapped}");
out
}
fn occurrences(content: &str, name: &str) -> Vec<usize> {
let mut out = Vec::new();
let mut from = 0;
while let Some(rel) = content[from..].find(name) {
let start = from + rel;
let end = start + name.len();
let before = content[..start].chars().next_back();
let after = content[end..].chars().next();
if !before.is_some_and(is_word) && !after.is_some_and(is_word) {
out.push(start);
}
from = end;
}
out
}
#[test]
fn a_split_token_is_copied_verbatim_rather_than_wrapped_into() {
let s = sibs(&["dev", "do"]);
for doc in [
"see {{ns:\ndev }} for the handoff\n",
"see {{ns: \n dev\n}} and {{tools:\ndev}} too\n",
] {
let (out, _) = templatize(doc, &s);
assert!(
!out.contains("{{ns:{{ns:") && !out.contains("\n{{ns:dev}} }}"),
"wrapping nested a token inside a split one: {out}"
);
assert_eq!(
expand(&out, &Some("jk".into()), &s, &HashSet::new()),
expand(doc, &Some("jk".into()), &s, &HashSet::new()),
"the rewrite changed what install expands: {out}"
);
}
let (out, n) = templatize("see {{ns:\ndev }} then do\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "see {{ns:\ndev }} then {{ns:do}}\n");
}
#[test]
fn fix_clears_prose_mentions_behind_every_desyncing_fence_shape() {
let s = sibs(&["dev"]);
for doc in [
"````md\n```\nreply here\n````\nThen see the {{ns:dev}} skill.\n",
"~~~md\n```\nreply here\n~~~\nThen see the {{ns:dev}} skill.\n",
"~~~md\n```\nreply here\n~~~\nThen see the dev skill.\n",
"```mind sync``` at line start\n\nThen see the {{ns:dev}} skill.\n",
] {
let fixed = fix_passes(doc, &s);
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"--fix left an unguarded reference in: {fixed}"
);
assert!(
fixed.contains("{{ns:dev}} skill"),
"the prose reference must end up tokenized: {fixed}"
);
}
}
#[test]
fn fix_clears_prose_mentions_past_an_indented_block_or_an_escaped_backtick() {
let s = sibs(&["dev"]);
for doc in [
"Wrap the reply in a fence, opened by:\n\n ```\n\nThen see the {{ns:dev}} skill.\n",
"Wrap the reply in a fence, opened by:\n\n ```\n\nThen see the dev skill.\n",
"Escape it with \\` in prose. Then see the {{ns:dev}} skill, then run `mind sync`.\n",
"Escape it with \\` in prose. Then see the dev skill, then run `mind sync`.\n",
] {
let fixed = fix_passes(doc, &s);
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"--fix left an unguarded reference in: {fixed}"
);
assert!(
fixed.contains("{{ns:dev}} skill"),
"the prose reference must end up tokenized: {fixed}"
);
}
}
#[test]
fn tilde_fences_take_an_info_string_and_close_only_on_a_bare_run() {
let doc = "~~~ruby `x`\n{{ns:dev}}\n~~~ still open\n{{ns:do}}\n~~~\nprose {{ns:dev}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::CodeBlock),
("dev".into(), NsContext::Prose),
]
);
let doc = "~~~\n{{ns:dev}}\n~~~~~\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
}
#[test]
fn an_unclosed_fence_runs_to_the_end_of_the_document() {
let doc = "```sh\necho {{ns:dev}}\n\nstill fenced {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::CodeBlock),
]
);
let (out, n) = unwrap_misplaced(doc, false);
assert_eq!(n, 2, "both tokens are un-wrapped: {out}");
}
#[test]
fn crlf_documents_classify_and_rewrite_like_lf_ones() {
let doc = "````md\r\n```sh\r\n{{ns:dev}}\r\n```\r\n````\r\n\r\n\
Read it with `gh auth\r\ntoken` then {{ns:do}} runs.\r\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
let s = sibs(&["dev", "do"]);
let (out, n) = templatize("a\r\n\r\nRun `gh auth\r\ntoken` then dev.\r\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "a\r\n\r\nRun `gh auth\r\ntoken` then {{ns:dev}}.\r\n");
}
#[test]
fn a_code_span_may_contain_brace_pairs() {
let doc = "see `{{ns:dev}}` and `a }} b` then {{ns:do}} here\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeSpan),
("do".into(), NsContext::Prose),
]
);
let s = sibs(&["dev"]);
let (out, n) = templatize("`{{` and `}}` then dev\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "`{{` and `}}` then {{ns:dev}}\n");
}
#[test]
fn a_token_on_a_fence_delimiter_line_is_not_scanned() {
let doc = "```{{ns:dev}}\nbody\n```\nprose {{ns:do}}\n";
assert_eq!(contexts(doc), vec![("do".into(), NsContext::Prose)]);
let (out, n) = unwrap_misplaced(doc, false);
assert_eq!(n, 0, "nothing on a delimiter line is rewritten: {out}");
assert_eq!(out, doc);
let doc = "```\n``` {{ns:dev}}\n```\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
}
#[test]
fn a_table_row_bounds_a_code_span() {
let doc = "| col | note |\n|---|---|\n| a ` b | see {{ns:dev}} |\n\
| c ` d | and {{ns:do}} |\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::Prose),
]
);
let doc = "| a | `{{ns:dev}}` | {{ns:do}} |\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeSpan),
("do".into(), NsContext::Prose),
]
);
}
#[test]
fn an_indented_fence_line_opens_a_block() {
let doc = "- item:\n\n ```sh\n echo {{ns:dev}}\n ```\n\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
}
#[test]
fn a_fence_opens_inside_a_list_item_whose_content_column_is_four() {
let doc = "1. step:\n\n ```sh\n echo {{ns:dev}}\n ```\n\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
let doc =
"- outer\n - inner\n\n ```sh\n echo {{ns:dev}}\n ```\n\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
let doc = "- outer\n - inner:\n\n ```\n\n still the item, {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
}
#[test]
fn an_indented_code_block_containing_a_lone_fence_line_is_not_a_fence() {
let doc = "Wrap the reply in a fence, opened by:\n\n ```\n\n\
Then see the {{ns:dev}} skill.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "Sample:\n\n mind learn {{ns:dev}}\n\nprose {{ns:do}} here\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
let s = sibs(&["dev", "do"]);
let (out, n) = templatize("Sample:\n\n mind learn dev\n\nprose do here\n", &s);
assert_eq!(n, 1, "only the prose mention is wrapped: {out}");
assert!(out.contains(" mind learn dev\n"), "{out}");
}
#[test]
fn an_unclosed_fence_in_a_list_item_ends_with_the_item() {
let doc = "- item:\n\n ```sh\n echo hi\n\nBack at top level, {{ns:dev}}.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "- item:\n\n ```sh\n echo {{ns:dev}}\n\ntop level {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
let doc = "```sh\necho hi\n\nstill fenced {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
}
#[test]
fn an_indented_line_that_continues_a_paragraph_is_still_prose() {
let doc = "This sentence runs long and wraps\n onto {{ns:dev}} here.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "This sentence ends.\n\n onto {{ns:dev}} here.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
let doc = "# Title\n {{ns:dev}} here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
}
#[test]
fn a_backslash_escaped_backtick_does_not_open_a_code_span() {
let doc = "Escape it with \\` in prose. Then see {{ns:dev}} and run `mind sync`.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let doc = "Literal \\\\` opens. Then {{ns:dev}} and `x`.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
let doc = "run `a \\` then {{ns:dev}} here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let s = sibs(&["dev"]);
let (out, n) = templatize("Escape it with \\` then see dev and run `x`.\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "Escape it with \\` then see {{ns:dev}} and run `x`.\n");
}
#[test]
fn fix_clears_mentions_that_used_to_be_permanently_unclearable() {
let s = sibs(&["dev"]);
for doc in [
"run `{{ns:dev}}` now\n",
"```sh\nmind learn {{ns:dev}}\n```\n",
"---\nname: thing\ndescription: hand off to dev\n---\nbody\n",
] {
let fixed = fix_passes(doc, &s);
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"no longer reported: {fixed}"
);
}
}
#[test]
fn templatize_skips_code_paths_and_frontmatter() {
let s = sibs(&["dev", "do"]);
let doc = "---\nname: dev\n---\nuse dev here\n`dev`\n~/dev\n```\nfor x; do\n```\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "only the prose mention is wrapped: {out}");
assert!(out.contains("use {{ns:dev}} here"), "{out}");
assert!(out.contains("`dev`"), "code span untouched: {out}");
assert!(out.contains("~/dev"), "path untouched: {out}");
assert!(out.contains("for x; do"), "code block untouched: {out}");
assert!(out.contains("name: dev"), "frontmatter untouched: {out}");
}
#[test]
fn templatize_wraps_the_description_field_but_never_name() {
let s = sibs(&["dev", "review"]);
let doc = "---\nname: review\ndescription: hand off to dev when done\n---\nbody\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "only the description mention is wrapped: {out}");
assert!(
out.contains("description: hand off to {{ns:dev}} when done"),
"{out}"
);
assert!(out.contains("name: review"), "name: stays bare: {out}");
let (again, m) = templatize(&out, &s);
assert_eq!(again, out);
assert_eq!(m, 0);
}
#[test]
fn templatize_never_wraps_a_structured_frontmatter_field() {
let s = sibs(&["dev", "review", "detect", "description"]);
for doc in [
"---\nname: review\nrequires: agent:dev\n---\nbody\n",
"---\nname: review\nbuild: make detect\n---\nbody\n",
"---\nname: review\ninstall: ./setup dev\n---\nbody\n",
"---\nname: review\nuninstall: ./teardown dev\n---\nbody\n",
"---\nname: review\nbin: detect\n---\nbody\n",
"---\nname: review\nmodel: dev\n---\nbody\n",
"---\nname: review\ndescription: a runner\n---\nbody\n",
] {
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"a structured frontmatter field must never be wrapped: {doc}"
);
}
assert_eq!(
unguarded_refs("---\nname: review\nrequires: agent:dev\n---\nbody\n", &s),
Vec::<String>::new()
);
}
fn surviving(doc: &str) -> String {
unwrap_misplaced(doc, false).0
}
#[test]
fn a_fence_opens_at_the_content_column_of_every_list_marker_shape() {
for (marker, indent) in [
("- ", " "),
("* ", " "),
("+ ", " "),
("- ", " "),
("1. ", " "),
("1) ", " "),
("10. ", " "),
("1. ", " "),
("123456789. ", " "),
("\t- ", "\t "),
] {
let doc = format!(
"{marker}step:\n\n{indent}```sh\n{indent}echo {{{{ns:dev}}}}\n\
{indent}```\n\nprose {{{{ns:do}}}}\n"
);
assert_eq!(
contexts(&doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
],
"marker {marker:?}:\n{doc}"
);
}
for line in ["-item", "1.step", "text"] {
let doc = format!("{line}\n\n ```\n\nThen see {{{{ns:dev}}}}.\n");
assert_eq!(
contexts(&doc),
vec![("dev".into(), NsContext::Prose)],
"{line:?} opens no list, so the indented ``` is code, not a fence"
);
}
}
#[test]
fn a_tab_indented_block_is_code_at_document_level() {
let doc = "Sample:\n\n\tmind learn {{ns:dev}}\n\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
]
);
let s = sibs(&["dev", "do"]);
let (out, n) = templatize("Sample:\n\n\tmind learn dev\n\nprose do\n", &s);
assert_eq!(n, 1, "only the prose mention is wrapped: {out}");
assert!(out.contains("\tmind learn dev\n"), "{out}");
}
#[test]
fn a_blank_line_keeps_a_list_item_open_and_a_dedent_closes_it() {
let doc = "- item:\n\n ```sh\n echo {{ns:a}}\n ```\n\n Then {{ns:b}}.\n";
assert_eq!(
contexts(doc),
vec![
("a".into(), NsContext::CodeBlock),
("b".into(), NsContext::Prose)
],
"a blank line inside the item does not end it"
);
let doc = "- outer\n - inner\n\n text {{ns:a}}\n\nback at zero\n\n {{ns:b}} here\n";
assert_eq!(
contexts(doc),
vec![
("a".into(), NsContext::Prose),
("b".into(), NsContext::CodeBlock)
],
"the inner item's content column is 4, so ` text` is prose; after \
the dedent the baseline is 0 again, so the same indent is code"
);
let doc = "- one\n- two\n- three\n\n {{ns:a}} deep\n";
assert_eq!(contexts(doc), vec![("a".into(), NsContext::CodeBlock)]);
}
#[test]
fn an_escape_never_reaches_across_a_line_break() {
let doc = "A trailing backslash \\\nthen `run {{ns:dev}} now` here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
let doc = "A trailing backslash \\\r\nthen `run {{ns:dev}} now` here\r\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
let doc = "Wrapped line \nthen `run {{ns:dev}} now` here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
let doc =
"para one ends \\\n\n\\` opens nothing, so {{ns:dev}} is prose and `x` is a span\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
}
#[test]
fn an_escaped_backtick_closes_an_open_span_and_is_literal_after_it() {
let doc = "run `a \\` then {{ns:one}} and \\` still prose {{ns:two}} and `x`\n";
assert_eq!(
contexts(doc),
vec![
("one".into(), NsContext::Prose),
("two".into(), NsContext::Prose)
],
"the span is `a \\`; both later tokens sit in prose"
);
let doc = "literal \\\\` opens a span with {{ns:dev}} in it `\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
let doc = "literal \\\\\\` opens nothing, {{ns:dev}} is prose, `x` is a span\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
}
#[test]
fn a_word_after_a_multibyte_character_is_measured_in_bytes() {
let doc = "R\u{e9}sum\u{e9}: run `mind learn {{ns:dev}}` then see {{ns:do}}.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeSpan),
("do".into(), NsContext::Prose)
]
);
let s = sibs(&["dev"]);
let (out, n) = templatize("R\u{e9}sum\u{e9}: run `dev` then see dev.\n", &s);
assert_eq!(n, 1, "only the mention outside the span is wrapped: {out}");
assert_eq!(out, "R\u{e9}sum\u{e9}: run `dev` then see {{ns:dev}}.\n");
let (out, n) = templatize("caf\u{e9} `gh auth\ntoken` then dev.\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "caf\u{e9} `gh auth\ntoken` then {{ns:dev}}.\n");
}
#[test]
fn a_spaced_thematic_break_is_a_break_and_not_a_list_marker() {
let doc = "intro\n\n* * *\n\n mind learn {{ns:dev}}\n";
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::CodeBlock)],
"four columns under a break is an indented code block"
);
let s = sibs(&["dev"]);
let (out, n) = templatize("intro\n\n* * *\n\n mind learn dev\n", &s);
assert_eq!(n, 0, "nothing in the code sample is wrapped: {out}");
let doc = "intro\n\n* * *\n\n ```\n\nThen see {{ns:dev}}.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
let doc = "intro\n\n* * *\n\n ```\n echo hi\n ```\n\nThen see {{ns:dev}}.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
}
#[test]
fn a_fence_opened_on_a_list_marker_line_is_still_a_fence() {
let doc = "Setup:\n\n- ```sh\n mind sync\n ```\n\n Then see the {{ns:dev}} skill.\n";
let s = sibs(&["dev"]);
assert_eq!(
unguarded_refs(&fix_passes(doc, &s), &s),
Vec::<String>::new(),
"NS-48"
);
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"the item's trailing paragraph is prose, not code"
);
assert_eq!(surviving(doc), doc, "`--fix` must not touch it");
}
#[test]
fn a_thematic_break_ends_the_paragraph_a_code_span_may_cross() {
let doc = "Type a lone ` in prose\n---\nThen see {{ns:dev}} and run `mind sync`.\n";
let s = sibs(&["dev"]);
assert_eq!(
unguarded_refs(&fix_passes(doc, &s), &s),
Vec::<String>::new(),
"NS-48"
);
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"setext"
);
assert_eq!(surviving(doc), doc);
let doc = "Type a lone ` in prose\n***\nThen see {{ns:dev}} and run `mind sync`.\n";
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"break"
);
assert_eq!(surviving(doc), doc);
let doc = "Type a lone ` in prose\n<div>\nThen see {{ns:dev}} and run `mind sync`.\n";
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"html"
);
assert_eq!(surviving(doc), doc);
let s = sibs(&["dev"]);
assert_eq!(
unguarded_refs(&fix_passes(doc, &s), &s),
Vec::<String>::new()
);
}
#[test]
fn a_setext_heading_leaves_no_paragraph_open_behind_it() {
let doc = "Title\n---\n mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
let s = sibs(&["dev"]);
let (out, n) = templatize("Title\n---\n mind learn dev\n", &s);
assert_eq!(n, 0, "nothing in the code block is wrapped: {out}");
let doc = "Some prose\n***\n mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
}
#[test]
fn a_token_broken_across_a_line_break_is_left_alone() {
let s = sibs(&["dev"]);
let doc = "see {{ns:\ndev }} for the handoff\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 0, "a split token is left alone, not nested: {out}");
assert_eq!(out, doc);
}
#[test]
fn a_fence_inside_a_blockquote_is_a_fence() {
let s = sibs(&["dev"]);
let (out, n) = templatize("> ```sh\n> mind learn dev\n> ```\n", &s);
assert_eq!(n, 0, "quoted code is not prose: {out}");
assert_eq!(out, "> ```sh\n> mind learn dev\n> ```\n");
let doc = "> ```sh\n> mind learn {{ns:dev}}\n> ```\n\nThen see {{ns:do}}.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
]
);
let doc = "> Sample:\n>\n> mind learn {{ns:dev}}\n>\n> Then see {{ns:do}}.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
]
);
let (out, n) = templatize(
"> Sample:\n>\n> mind learn dev\n>\n> Then see do.\n",
&s,
);
assert_eq!(n, 0, "only `do` is a sibling here: {out}");
let s2 = sibs(&["dev", "do"]);
let (out, n) = templatize(
"> Sample:\n>\n> mind learn dev\n>\n> Then see do.\n",
&s2,
);
assert_eq!(n, 1, "the quoted prose mention is wrapped: {out}");
assert_eq!(
out,
"> Sample:\n>\n> mind learn dev\n>\n> Then see {{ns:do}}.\n"
);
}
#[test]
fn unwrap_misplaced_restores_words() {
let doc = "prose {{ns:dev}}\n`{{ns:test}}`\n~/{{ns:dev}}\n";
let (out, n) = unwrap_misplaced(doc, false);
assert_eq!(
n, 2,
"code-span and path tokens un-wrapped, prose kept: {out}"
);
assert_eq!(out, "prose {{ns:dev}}\n`test`\n~/dev\n");
let (all, m) = unwrap_misplaced(doc, true);
assert_eq!(m, 3);
assert_eq!(all, "prose dev\n`test`\n~/dev\n");
}
fn wraps(doc: &str, name: &str) -> bool {
let (out, n) = templatize(doc, &sibs(&[name]));
assert!(n <= 1, "more than one wrap in:\n{doc}\n{out}");
n == 1
}
#[test]
fn an_html_block_is_prose_and_its_preformatted_content_is_not_protected() {
let doc = "<div align=\"center\">\nSee {{ns:dev}} for details.\n</div>\n";
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"a token inside an HTML block survives --fix"
);
assert_eq!(surviving(doc), doc);
assert!(wraps("<div>\nHand off to dev.\n</div>\n", "dev"));
assert!(
wraps("Sample:\n\n<pre>\nmind learn dev\n</pre>\n", "dev"),
"known gap: a `<pre>` code sample is not protected the way a fence is"
);
let doc = "A <b>bold</b> word, then {{ns:dev}} here.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc);
}
#[test]
fn an_html_block_bounds_the_paragraph_a_code_span_may_cross() {
let doc = "Type a lone ` in prose\n<div>\n</div>\n\n\
Then see {{ns:dev}} and run `mind sync`.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc);
}
#[test]
fn a_link_is_prose_and_its_text_is_what_wrapping_is_meant_to_reach() {
let doc = "See [the docs](https://example.com/x) and {{ns:dev}}.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc);
assert!(
!wraps("See [the docs](https://example.com/dev).\n", "dev"),
"a name after a `/` is path-adjacent and is left alone"
);
let (out, n) = templatize("See [the dev skill](x.md).\n", &sibs(&["dev"]));
assert_eq!(n, 1);
assert_eq!(out, "See [the {{ns:dev}} skill](x.md).\n");
}
#[test]
fn wrapping_leaves_link_syntax_alone() {
let s = sibs(&["dev"]);
for doc in [
"[dev]: https://example.com/x\n\nSee [the docs][dev].\n",
"See [the docs](dev.md).\n",
] {
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"link syntax must not be rewritten"
);
}
}
#[test]
fn every_part_of_a_link_but_its_text_is_syntax() {
let s = sibs(&["dev"]);
for doc in [
"See [the docs](dev.md).\n",
"See [the docs](docs/dev.md).\n",
"See [the docs](https://example.com/x#dev).\n",
"See [the docs](x.md \"the dev notes\").\n",
"See [the docs](x.md 'the dev notes').\n",
"See [the docs](x.md (the dev notes)).\n",
"See [the docs][dev].\n\n[dev]: https://example.com/x\n",
"[dev]: https://example.com/x \"the dev notes\"\n\nSee [the docs][dev].\n",
"[dev]: https://example.com/x\n\nSee [dev] for details.\n",
"[dev]: https://example.com/x\n\nSee [dev][] for details.\n",
"\n",
"Mail <dev@example.com> about it.\n",
"See <https://example.com?q=dev> now.\n",
"Mail <mailto:dev@example.com> about it.\n",
] {
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"link syntax must not be rewritten:\n{doc}"
);
}
let (out, n) = templatize("See [the dev skill](x.md).\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "See [the {{ns:dev}} skill](x.md).\n");
let (out, n) = templatize("See [the dev skill][docs].\n\n[docs]: x.md\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "See [the {{ns:dev}} skill][docs].\n\n[docs]: x.md\n");
let (out, n) = templatize("See [the **dev** skill](x.md).\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "See [the **{{ns:dev}}** skill](x.md).\n");
let (out, n) = templatize("See [the docs](dev.md), then dev.\n", &s);
assert_eq!(n, 1, "only the mention outside the link: {out}");
assert_eq!(out, "See [the docs](dev.md), then {{ns:dev}}.\n");
}
#[test]
fn a_token_in_link_syntax_is_misplaced_and_fix_takes_it_out() {
let s = sibs(&["dev"]);
for (doc, fixed) in [
(
"See [the docs]({{ns:dev}}.md) now.\n",
"See [the docs](dev.md) now.\n",
),
(
"See [the docs](x.md \"{{ns:dev}} notes\") now.\n",
"See [the docs](x.md \"dev notes\") now.\n",
),
(
"[{{ns:dev}}]: https://example.com/x\n\nSee [the docs][{{ns:dev}}].\n",
"[dev]: https://example.com/x\n\nSee [the docs][dev].\n",
),
] {
assert!(
contexts(doc)
.iter()
.all(|(name, ctx)| name == "dev" && *ctx == NsContext::Path),
"every token here is misplaced link syntax: {:?}",
contexts(doc)
);
assert_eq!(surviving(doc), fixed, "`--fix` must un-wrap it");
assert_eq!(fix_passes(doc, &s), fixed, "and must not put it back");
assert_eq!(fix_passes(fixed, &s), fixed, "idempotent");
}
}
#[test]
fn a_link_only_mention_is_no_longer_a_false_positive() {
let s = sibs(&["dev"]);
for doc in [
"See [the docs](dev.md).\n",
"[dev]: https://example.com/x\n\nSee [the docs][dev].\n",
] {
let fixed = fix_passes(doc, &s);
assert_eq!(fixed, doc, "wrapping must not rewrite link syntax: {fixed}");
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"link syntax is not a reference and is no longer reported: {fixed}"
);
}
}
#[test]
fn a_list_only_interrupts_a_paragraph_when_commonmark_says_it_does() {
let doc = "Intro line\n2. Step:\n\n mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
assert!(!wraps(
"Intro line\n2. Step:\n\n mind learn dev\n",
"dev"
));
let doc = "Intro line\n1. Step:\n\n mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert!(wraps("Intro line\n1. Step:\n\n mind learn dev\n", "dev"));
let doc = "Intro line\n- Step:\n\n mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
}
#[test]
fn an_atx_heading_is_prose_and_bounds_an_inline_run() {
let doc = "## Hand off to {{ns:dev}} ##\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
let (out, n) = templatize("## Hand off to dev ##\n", &sibs(&["dev"]));
assert_eq!(n, 1);
assert_eq!(out, "## Hand off to {{ns:dev}} ##\n");
let doc = "# A lone ` in a heading\nThen see {{ns:dev}} and run `x`.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc);
for doc in ["####### {{ns:dev}} here\n", "#{{ns:dev}} here\n"] {
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"{doc}"
);
}
}
#[test]
fn a_nested_blockquote_carries_its_code_block_like_any_other_container() {
let doc = "> > ```sh\n> > mind learn {{ns:dev}}\n> > ```\n>\n> Then see {{ns:do}}.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
]
);
let s = sibs(&["dev", "do"]);
let (out, n) = templatize(
"> > ```sh\n> > mind learn dev\n> > ```\n>\n> Then see do.\n",
&s,
);
assert_eq!(n, 1, "only the quoted prose mention is wrapped: {out}");
assert_eq!(
out,
"> > ```sh\n> > mind learn dev\n> > ```\n>\n> Then see {{ns:do}}.\n"
);
let doc = "> - item:\n>\n> ```sh\n> echo {{ns:dev}}\n> ```\n>\n> Then {{ns:do}}.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
]
);
}
#[test]
fn a_table_cell_cannot_hold_a_code_block() {
let doc = "| a | b |\n|---|---|\n| ``` | see {{ns:dev}} |\n| x | and {{ns:do}} |\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::Prose)
]
);
assert_eq!(surviving(doc), doc);
let doc = "| a | `x | {{ns:dev}}` |\n|---|---|\n| c | {{ns:do}} |\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeSpan),
("do".into(), NsContext::Prose)
]
);
}
#[test]
fn a_hard_line_break_does_not_stop_the_block_below_it() {
for br in ["\\", " "] {
let doc = format!(
"Line one{br}\n```sh\nmind learn {{{{ns:dev}}}}\n```\n\nThen {{{{ns:do}}}}.\n"
);
assert_eq!(
contexts(&doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
],
"break {br:?}"
);
}
let doc = "run `gh auth\\\ntoken {{ns:dev}}` now\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
}
#[test]
fn a_loose_list_item_is_prose_and_a_tight_one_is_too() {
let tight = "- see {{ns:dev}}\n- run `{{ns:do}}`\n";
let loose = "- see {{ns:dev}}\n\n- run `{{ns:do}}`\n";
for doc in [tight, loose] {
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::CodeSpan)
],
"{doc}"
);
}
let s = sibs(&["dev", "do"]);
for doc in ["- see dev\n- run `do`\n", "- see dev\n\n- run `do`\n"] {
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "only the prose mention is wrapped: {out}");
assert!(out.contains("`do`"), "the span is untouched: {out}");
}
}
#[test]
fn an_autolink_is_prose_and_a_url_path_segment_is_not_wrapped() {
assert!(!wraps("See <https://example.com/dev> now.\n", "dev"));
assert!(!wraps("See <https://example.com/dev/x> now.\n", "dev"));
assert!(!wraps("See <https://dev.example.com/> now.\n", "dev"));
let doc = "See <https://example.com/{{ns:dev}}> now.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Path)]);
let doc = "A & B, then {{ns:dev}} and `{{ns:do}}`.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::CodeSpan)
]
);
}
#[test]
fn a_three_backtick_run_carrying_a_backtick_is_a_span_and_not_a_fence() {
let doc = "```a ` b``` opens nothing\n\nprose {{ns:dev}} here\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc);
let doc = "```a b\nstill fenced {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
let doc = "~~~a ` b\n{{ns:dev}}\n~~~\nprose {{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose)
]
);
}
#[test]
fn a_thematic_break_of_any_spelling_opens_no_list() {
for brk in ["---", "***", "___", "* * *", "- - -", " ***"] {
let doc = format!("intro\n\n{brk}\n\n mind learn {{{{ns:dev}}}}\n");
assert_eq!(
contexts(&doc),
vec![("dev".into(), NsContext::CodeBlock)],
"break {brk:?}"
);
assert!(
!wraps(&format!("intro\n\n{brk}\n\n mind learn dev\n"), "dev"),
"break {brk:?}"
);
}
}
#[test]
fn no_construct_leaks_its_code_into_the_prose_paragraph_after_it() {
let s = sibs(&["dev", "do"]);
for prefix in [
"",
"# Heading\n\n",
"## Heading ##\n\n",
"Setext\n---\n\n",
"***\n\n",
"* * *\n\n",
"```sh\nmind sync\n```\n\n",
"````md\n```sh\nmind sync\n```\n````\n\n",
"~~~text\n```\n~~~\n\n",
"```a ` b``` is a span\n\n",
" indented code\n\n",
"\ttab indented code\n\n",
"> quoted prose\n\n",
"> ```sh\n> mind sync\n> ```\n\n",
"> > ```sh\n> > mind sync\n> > ```\n\n",
"- item\n- item\n\n",
"- item:\n\n ```sh\n mind sync\n ```\n\n",
"1. item:\n\n ```sh\n mind sync\n ```\n\n",
"- ```sh\n mind sync\n ```\n\n",
"| a | b |\n|---|---|\n| `x` | y |\n\n",
"<div>\nraw html\n</div>\n\n",
"<https://example.com/x>\n\n",
"[label]: https://example.com/x\n\n",
"Escape a backtick as \\` in prose.\n\n",
"A lone ` backtick in prose.\n\n",
"Trailing backslash \\\n\n",
"Trailing spaces \n\n",
"---\nname: thing\ndescription: x\n---\n",
"Run `gh auth\ntoken --hostname x` inline.\n\n",
"``a ` b`` is a span.\n\n",
"Text with {{tools:x}} and {{self}} tokens.\n\n",
] {
let doc = format!("{prefix}Then see the {{{{ns:dev}}}} skill.\n");
assert_eq!(
contexts(&doc),
vec![("dev".into(), NsContext::Prose)],
"prefix {prefix:?} leaked into the paragraph after it:\n{doc}"
);
assert_eq!(surviving(&doc), doc, "prefix {prefix:?}");
let bare = format!("{prefix}Then see the do skill.\n");
let fixed = fix_passes(&bare, &s);
assert!(
fixed.contains("{{ns:do}} skill"),
"prefix {prefix:?} kept wrapping from reaching prose:\n{fixed}"
);
assert_eq!(
unguarded_refs(&fixed, &sibs(&["do"])),
Vec::<String>::new(),
"prefix {prefix:?} left an unguarded reference:\n{fixed}"
);
}
}
#[test]
fn frontmatter_delimiters_are_read_at_their_documented_edges() {
let s = sibs(&["dev", "do"]);
assert!(!wraps("---\nname: dev\n---", "dev"));
let (out, n) = templatize("--- \nname: dev\n--- \nsee do\n", &s);
assert_eq!(n, 1, "only the body mention is wrapped: {out}");
assert_eq!(out, "--- \nname: dev\n--- \nsee {{ns:do}}\n");
let (out, n) = templatize("---\nname: dev\n---\nsee do", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "---\nname: dev\n---\nsee {{ns:do}}");
for doc in ["", "---", "---\n", "\n", " \n"] {
assert_eq!(templatize(doc, &s), (doc.to_string(), 0), "{doc:?}");
assert!(scan_ns_refs(doc).is_empty(), "{doc:?}");
}
}
#[test]
fn an_unterminated_frontmatter_block_swallows_the_whole_document() {
let s = sibs(&["dev"]);
let doc = "---\nname: thing\ndescription: x\n\nSee dev in the body.\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let tokenized = "---\nname: thing\n\nSee {{ns:dev}} in the body.\n";
assert_eq!(contexts(tokenized), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(tokenized), tokenized);
assert_eq!(
unguarded_refs(&fix_passes(doc, &s), &s),
Vec::<String>::new(),
"and the structure-aware check does not report what wrapping declines"
);
let doc = "---\n\nIntro mentioning dev.\n\n---\n\nBody mentioning dev.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "only the text past the second delimiter: {out}");
assert_eq!(
out,
"---\n\nIntro mentioning dev.\n\n---\n\nBody mentioning {{ns:dev}}.\n"
);
}
#[test]
fn a_setext_underline_is_not_mistaken_for_a_frontmatter_delimiter() {
let s = sibs(&["dev", "do"]);
let (out, n) = templatize("Title\n---\nSee dev here.\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "Title\n---\nSee {{ns:dev}} here.\n");
let (out, n) = templatize("dev\n---\nSee do here.\n", &s);
assert_eq!(n, 2, "{out}");
assert_eq!(out, "{{ns:dev}}\n---\nSee {{ns:do}} here.\n");
}
#[test]
fn a_utf8_bom_does_not_expose_the_frontmatter_to_wrapping() {
let doc = "\u{feff}---\nname: dev\ndescription: x\n---\nBody text.\n";
assert_eq!(
templatize(doc, &sibs(&["dev"])),
(doc.to_string(), 0),
"the `name:` field must survive a BOM"
);
}
#[test]
fn a_bom_is_stripped_without_shifting_any_offset() {
let doc = "\u{feff}---\nname: thing\ndescription: caf\u{e9}\n---\n\
`x`{{ns:dev}} and `{{ns:do}}`\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::CodeSpan)
]
);
assert_eq!(
surviving(doc),
"\u{feff}---\nname: thing\ndescription: caf\u{e9}\n---\n\
`x`{{ns:dev}} and `do`\n"
);
let s = sibs(&["dev"]);
let (out, n) = templatize(
"\u{feff}---\nname: thing\ndescription: hands off to dev\n---\nSee dev here.\n",
&s,
);
assert_eq!(n, 2, "the description mention and the body mention: {out}");
assert_eq!(
out,
"\u{feff}---\nname: thing\ndescription: hands off to {{ns:dev}}\n---\n\
See {{ns:dev}} here.\n"
);
let (out, n) = templatize("\u{feff}# Title\n\nSee dev here.\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "\u{feff}# Title\n\nSee {{ns:dev}} here.\n");
}
#[test]
fn the_frontmatter_shift_keeps_every_body_offset_exact() {
let doc = "---\ndescription: caf\u{e9} r\u{e9}sum\u{e9} \u{1d400}\n---\n\
`x`{{ns:dev}} and `{{ns:do}}`{{ns:dev}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::CodeSpan),
("dev".into(), NsContext::Prose)
]
);
assert_eq!(
surviving(doc),
"---\ndescription: caf\u{e9} r\u{e9}sum\u{e9} \u{1d400}\n---\n\
`x`{{ns:dev}} and `do`{{ns:dev}}\n"
);
let doc = "---\nname: thing\n---\nCaf\u{e9} \u{1d400}: `x`{{ns:dev}} and `{{ns:do}}`\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::CodeSpan)
]
);
let doc = "---\r\ndescription: caf\u{e9}\r\n---\r\n`x`{{ns:dev}} and `{{ns:do}}`\r\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::CodeSpan)
]
);
}
#[test]
fn an_unterminated_brace_span_stops_wrapping_at_the_document_level() {
let s = sibs(&["dev", "do"]);
let doc = "Use {{ v } here. Then hand off to dev.\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let doc = "Use {{ v } then dev. Later {{ns:do}} ends it. And dev again.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(
out,
"Use {{ v } then dev. Later {{ns:do}} ends it. And {{ns:dev}} again.\n"
);
assert_eq!(
templatize("see dev{{", &s),
("see {{ns:dev}}{{".to_string(), 1)
);
let doc = "Use {{ v } then `{{ns:dev}}` here, and do.\n";
let once = fix_passes(doc, &s);
assert_eq!(once, fix_passes(&once, &s), "{once}");
}
#[test]
fn a_path_adjacent_mention_is_no_longer_a_false_positive() {
let s = sibs(&["dev"]);
for doc in [
"Config lives in ~/dev now.\n",
"Config lives in etc/dev/x now.\n",
] {
let fixed = fix_passes(doc, &s);
assert_eq!(fixed, doc, "wrapping must not rewrite a path: {fixed}");
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"a path-adjacent mention is not a reference and is no longer reported: {fixed}"
);
}
assert!(wraps("Config lives in ~/ dev now.\n", "dev"));
}
#[test]
fn a_token_on_a_delimiter_line_survives_an_all_code_un_wrap() {
let script = "#!/bin/sh\n# hand off to {{ns:dev}}\ncat <<'EOF'\n\
```{{ns:do}}\nEOF\n";
let (out, n) = unwrap_misplaced(script, true);
assert_eq!(n, 1, "only the token off the delimiter line: {out}");
assert_eq!(
out,
"#!/bin/sh\n# hand off to dev\ncat <<'EOF'\n```{{ns:do}}\nEOF\n"
);
let script = "#!/bin/sh\n# hand off to {{ns:dev}}\nrun {{ns:do}}\n";
assert_eq!(
unwrap_misplaced(script, true),
("#!/bin/sh\n# hand off to dev\nrun do\n".to_string(), 2)
);
}
#[test]
fn the_space_run_after_a_list_marker_sets_the_content_column() {
assert!(wraps("- Step:\n\n mind learn dev\n", "dev"));
assert!(!wraps("- Step:\n\n mind learn dev\n", "dev"));
assert!(!wraps("- mind learn dev\n", "dev"));
let doc = "- mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
assert!(!wraps("1234567890. Step:\n\n mind learn dev\n", "dev"));
let doc = "123456789. Step:\n\n mind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
}
#[test]
fn a_tab_advances_to_the_next_multiple_of_four_columns() {
assert!(!wraps("Sample:\n\n \tmind learn dev\n", "dev"));
assert!(!wraps("Sample:\n\n\t\tmind learn dev\n", "dev"));
assert!(!wraps("Sample:\n\n \tmind learn dev\n", "dev"));
let doc = "Sample:\n\n \tmind learn {{ns:dev}}\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeBlock)]);
assert!(wraps("- Step:\n\n \tmind learn dev\n", "dev"));
assert!(wraps("-\tStep:\n\n mind learn dev\n", "dev"));
assert!(!wraps("-\tStep:\n\n mind learn dev\n", "dev"));
}
#[test]
fn an_image_nested_in_a_links_text_is_syntax_in_both_layers() {
let s = sibs(&["dev", "do"]);
let (out, n) = templatize("See [](do.md) now.\n", &s);
assert_eq!(n, 1, "only the alt text is prose: {out}");
assert_eq!(out, "See [](do.md) now.\n");
let doc = "See []({{ns:do}}.md).\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::Prose),
("do".into(), NsContext::Path),
("do".into(), NsContext::Path),
]
);
assert_eq!(
surviving(doc),
"See [](do.md).\n"
);
let (out, n) = templatize("See [](y.md), then dev.\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "See [](y.md), then {{ns:dev}}.\n");
let doc = " inside](dev.png)\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 2, "both visible texts, neither destination: {out}");
assert_eq!(
out,
" inside](dev.png)\n"
);
let doc = "[]\n\n[]: y.md\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 2, "a bracketed label is not a label: {out}");
assert_eq!(
out,
"[]\n\n\
[]: y.md\n"
);
}
#[test]
fn a_link_in_a_table_cell_or_a_blockquote_is_still_a_link() {
let s = sibs(&["dev", "do"]);
let doc = "| step | link |\n|---|---|\n| one | [the dev notes](do.md) |\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "only the link text in the cell: {out}");
assert_eq!(
out,
"| step | link |\n|---|---|\n| one | [the {{ns:dev}} notes](do.md) |\n"
);
let doc = "> See [the dev notes](do.md) first.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "> See [the {{ns:dev}} notes](do.md) first.\n");
let doc = "> > See [the dev notes](do.md) first.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "> > See [the {{ns:dev}} notes](do.md) first.\n");
let doc = "1. Step:\n\n See [the dev notes](do.md).\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "1. Step:\n\n See [the {{ns:dev}} notes](do.md).\n");
let doc = "> See [the docs]({{ns:dev}}.md).\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Path)]);
assert_eq!(surviving(doc), "> See [the docs](dev.md).\n");
}
#[test]
fn a_code_span_inside_link_text_is_still_a_code_span() {
let s = sibs(&["dev", "do"]);
let doc = "See [the `dev` command](do.md) now.\n";
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"a name in a span inside link text is code, and the destination is syntax"
);
let doc = "See [the `{{ns:dev}}` command](x.md) now.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::CodeSpan)]);
assert_eq!(surviving(doc), "See [the `dev` command](x.md) now.\n");
let (out, n) = templatize("See [the dev `x` guide](y.md).\n", &s);
assert_eq!(n, 1, "{out}");
assert_eq!(out, "See [the {{ns:dev}} `x` guide](y.md).\n");
}
#[test]
fn an_angle_bracket_destination_is_syntax_like_any_other() {
let s = sibs(&["dev", "do"]);
for doc in [
"See [the docs](<my dev notes.md>).\n",
"See [the docs](<dev>).\n",
"\n",
] {
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"an angle-bracket destination is syntax: {doc}"
);
}
let doc = "See [the docs](<{{ns:dev}} notes.md>).\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Path)]);
assert_eq!(surviving(doc), "See [the docs](<dev notes.md>).\n");
}
#[test]
fn a_link_reference_definition_span_covers_it_and_stops_at_its_end() {
let s = sibs(&["dev", "do"]);
let doc = "[dev]: https://example.com/x \"the do notes\"\n\nSee [x][dev].\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0), "one-line def");
let doc = "[dev]: https://example.com/x\n \"the do notes\"\n\nSee [x][dev].\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0), "wrapped title");
let doc = "[dev]:\n https://example.com/do-notes\n\nSee [x][dev].\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0), "wrapped dest");
let doc = "[dev]: https://example.com/x\nThen see the do skill.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "the line after the def is prose: {out}");
assert_eq!(
out,
"[dev]: https://example.com/x\nThen see the {{ns:do}} skill.\n"
);
let doc = "[dev]: https://example.com/a\n[do]: https://example.com/b\n\n\
See [x][dev] and [y][do].\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0), "two defs");
}
#[test]
fn a_definition_inside_a_container_is_still_a_definition() {
let s = sibs(&["dev", "do"]);
for doc in [
"> [dev]: https://example.com/x\n\nSee [the docs][dev].\n",
"- [dev]: https://example.com/x\n\nSee [the docs][dev].\n",
"> [dev]: https://example.com/x\n\nSee [the docs][dev].\n",
] {
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"a definition in a container is syntax: {doc}"
);
}
let doc = "> [{{ns:dev}}]: https://example.com/x\n\nSee [the docs][{{ns:dev}}].\n";
assert!(
contexts(doc).iter().all(|(_, ctx)| *ctx == NsContext::Path),
"{:?}",
contexts(doc)
);
assert_eq!(
surviving(doc),
"> [dev]: https://example.com/x\n\nSee [the docs][dev].\n"
);
}
#[test]
fn a_definition_below_an_unterminated_frontmatter_block_is_never_read() {
let s = sibs(&["dev", "do"]);
let doc = "---\nname: thing\n[dev]: https://example.com/x\n\n\
See [the do notes][dev].\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let doc = "---\nname: thing\n\nSee [the docs]({{ns:dev}}.md).\n";
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::Prose)],
"an unterminated block makes everything a field, so nothing is deleted"
);
assert_eq!(surviving(doc), doc);
let doc = "---\nname: thing\n---\n\nSee [the do notes]({{ns:dev}}.md).\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Path)]);
assert_eq!(
templatize(&surviving(doc), &s),
(
"---\nname: thing\n---\n\nSee [the {{ns:do}} notes](dev.md).\n".to_string(),
1
)
);
}
#[test]
fn an_unresolved_reference_link_is_not_a_link_and_its_label_is_prose() {
let s = sibs(&["dev", "do"]);
let (out, n) = templatize("See [the docs][dev] now.\n", &s);
assert_eq!(n, 1, "an unresolved label is prose: {out}");
assert_eq!(out, "See [the docs][{{ns:dev}}] now.\n");
let doc = "See [the docs][dev] now.\n\n[dev]: https://example.com/x\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let doc = "See [the docs][{{ns:dev}}] now.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc);
assert_eq!(fix_passes(doc, &s), doc, "idempotent");
let doc = "[dev]: https://example.com/x\n\nSee [dev] for details.\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0), "declined");
let doc = "[dev]: https://example.com/x\n\nSee [{{ns:dev}}] for details.\n";
assert_eq!(contexts(doc), vec![("dev".into(), NsContext::Prose)]);
assert_eq!(surviving(doc), doc, "and never deleted");
}
#[test]
fn a_fence_info_string_mention_is_no_longer_a_false_positive() {
let s = sibs(&["dev"]);
for doc in ["```dev\nx\n```\n", "~~~ dev\nx\n~~~\n"] {
let fixed = fix_passes(doc, &s);
assert_eq!(fixed, doc, "an info string is never rewritten: {fixed}");
assert_eq!(
unguarded_refs(&fixed, &s),
Vec::<String>::new(),
"delimiter structure is not a reference and is no longer reported: {fixed}"
);
}
}
#[test]
fn a_bom_survives_every_frontmatter_spelling() {
let s = sibs(&["dev", "do"]);
let doc = "\u{feff}---\r\nname: dev\r\ndescription: see do\r\n---\r\nSee do here.\r\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 2, "the description mention and the body mention: {out}");
assert_eq!(
out,
"\u{feff}---\r\nname: dev\r\ndescription: see {{ns:do}}\r\n---\r\n\
See {{ns:do}} here.\r\n"
);
let doc = "\u{feff} ---\nname: dev\ndescription: see do\n--- \nSee do here.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 2, "the description mention and the body mention: {out}");
assert_eq!(
out,
"\u{feff} ---\nname: dev\ndescription: see {{ns:do}}\n--- \nSee {{ns:do}} here.\n"
);
let doc = "\u{feff}---\nname: {{ns:dev}}\n---\n`x`{{ns:do}}\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::FrontmatterName),
("do".into(), NsContext::Prose),
]
);
let doc = "\u{feff}---\nname: dev\n\nSee do in the body.\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let doc = "\u{feff}---\nname: thing\n---\n mind learn do\n";
assert_eq!(
templatize(doc, &s),
(doc.to_string(), 0),
"the body's first block is an indented code block, not a continuation"
);
assert_eq!(
contexts("\u{feff}---\nname: thing\n---\n mind learn {{ns:do}}\n"),
vec![("do".into(), NsContext::CodeBlock)]
);
let doc = "\u{feff}---\nname: aaaaaaaaaaa-{{ns:dev}}\n---\nbody\n";
assert_eq!(
contexts(doc),
vec![("dev".into(), NsContext::FrontmatterName)]
);
}
#[test]
fn a_bom_does_not_move_the_bytes_an_all_code_un_wrap_deletes() {
let script = "\u{feff}#!/bin/sh\n# hand off to {{ns:dev}}\nrun {{ns:do}}\n";
assert_eq!(
unwrap_misplaced(script, true),
(
"\u{feff}#!/bin/sh\n# hand off to dev\nrun do\n".to_string(),
2
)
);
let yaml = "\u{feff}---\nkey: {{ns:dev}}\nother: {{ns:do}}\n";
assert_eq!(
unwrap_misplaced(yaml, true),
("\u{feff}---\nkey: dev\nother: do\n".to_string(), 2)
);
assert_eq!(unwrap_misplaced(yaml, false), (yaml.to_string(), 0));
}
#[test]
fn a_bom_on_a_file_with_no_frontmatter_is_behind_its_block_structure() {
let s = sibs(&["dev", "do"]);
let doc = "\u{feff}```sh\nmind learn dev\n```\n\nThen see dev.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(
out, "\u{feff}```sh\nmind learn dev\n```\n\nThen see {{ns:dev}}.\n",
"the sample is code and the prose below it is wrapped"
);
let doc = "\u{feff}~~~sh\nmind learn dev\n~~~\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let doc = "\u{feff}Intro.\n\n~~~sh\nmind learn dev\n~~~\n";
assert_eq!(templatize(doc, &s), (doc.to_string(), 0));
let doc = "\u{feff}---\nname: thing\n---\n```sh\nmind learn dev\n```\n\nThen see do.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(
out,
"\u{feff}---\nname: thing\n---\n```sh\nmind learn dev\n```\n\n\
Then see {{ns:do}}.\n"
);
}
#[test]
fn a_bom_prefixed_file_with_no_frontmatter_still_sees_its_first_line_fence() {
let s = sibs(&["dev", "do"]);
let doc = "\u{feff}```sh\nmind learn dev\n```\n\nThen see do.\n";
let (out, n) = templatize(doc, &s);
assert_eq!(n, 1, "{out}");
assert_eq!(
out, "\u{feff}```sh\nmind learn dev\n```\n\nThen see {{ns:do}}.\n",
"the sample keeps its bare name and the prose below it is wrapped"
);
let doc = "\u{feff}```sh\nmind learn {{ns:dev}}\n```\n\nThen see {{ns:do}}.\n";
assert_eq!(
contexts(doc),
vec![
("dev".into(), NsContext::CodeBlock),
("do".into(), NsContext::Prose),
]
);
assert_eq!(
surviving(doc),
"\u{feff}```sh\nmind learn dev\n```\n\nThen see {{ns:do}}.\n",
"`--fix` must not delete the prose token below the fence"
);
}
#[test]
fn inert_tokens_reports_each_braced_span_as_written_deduped() {
assert_eq!(
inert_tokens("a {{ns:x}} b {{tools:t}} c {{ns:x}} d"),
vec!["{{ns:x}}".to_string(), "{{tools:t}}".to_string()],
);
assert_eq!(inert_tokens("{{ self }}"), vec!["{{ self }}".to_string()]);
assert!(inert_tokens("plain text, no tokens").is_empty());
}
#[test]
fn inert_tokens_agree_with_expanders_on_unterminated_and_empty() {
let store = Path::new("/m/store");
let none = None;
let c = ctx(store, &none, ItemKind::Skill, "self", &[]);
assert!(inert_tokens("#!/bin/sh\n{{tools:detect --scan\n").is_empty());
assert!(referenced_names("{{ns:x").is_empty());
assert_eq!(expand_paths("{{self", &c).unwrap(), "{{self");
assert_eq!(
expand("{{ns:x", &none, &sibs(&["x"]), &sibs(&[])).unwrap(),
"{{ns:x",
);
assert_eq!(inert_tokens("{{}}"), vec!["{{}}".to_string()]);
assert_eq!(expand_paths("{{}}", &c).unwrap(), "{{}}");
assert_eq!(
expand("{{}}", &none, &sibs(&[]), &sibs(&[])).unwrap(),
"{{}}",
);
}
#[test]
fn inert_tokens_never_misses_a_token_the_path_expander_would_expand() {
let store = Path::new("/m/store");
let none = None;
let sib = [psib(ItemKind::Tool, "detect", Some("detect"))];
let c = ctx(store, &none, ItemKind::Skill, "run", &sib);
let nested = "{{ {{tools:detect}} }}";
let expanded = expand_paths(nested, &c).unwrap();
assert_ne!(
expanded, nested,
"the inner path token really does expand once the outer passes through"
);
let reported = inert_tokens(nested);
assert!(
reported.iter().any(|t| t.contains("tools:detect")),
"inert_tokens must name a span covering the token expand_paths \
expands, never miss it: {reported:?}"
);
let nested_ns = "{{ {{ns:detect}} }}";
let ns_expanded = expand(nested_ns, &none, &sibs(&["detect"]), &sibs(&[])).unwrap();
assert_ne!(
ns_expanded, nested_ns,
"the inner ns token really does expand"
);
assert!(
inert_tokens(nested_ns)
.iter()
.any(|t| t.contains("ns:detect")),
"inert_tokens must also cover a nested ns token"
);
}
#[test]
fn inert_tokens_dedup_is_linear_and_correct_on_many_distinct_tokens() {
let mut content = String::new();
for i in 0..5000 {
content.push_str(&format!("{{{{ns:x{i}}}}} "));
}
content.push_str("{{ns:x0}} {{ns:x4999}} ");
let tokens = inert_tokens(&content);
assert_eq!(
tokens.len(),
5000,
"5000 distinct tokens plus 2 repeats must dedup to exactly 5000"
);
assert_eq!(tokens[0], "{{ns:x0}}");
assert_eq!(tokens[1], "{{ns:x1}}");
assert_eq!(tokens[4999], "{{ns:x4999}}");
let unique: HashSet<&String> = tokens.iter().collect();
assert_eq!(
unique.len(),
tokens.len(),
"every reported token is distinct"
);
}
#[test]
fn strip_braced_leaves_an_unterminated_token_verbatim() {
let sib = [
psib(ItemKind::Tool, "before", None),
psib(ItemKind::Tool, "after", None),
];
let content = "{{tools:before}} unterminated {{tools:after and more text";
let found = bare_tool_refs(content, &sib);
assert!(
!found.contains(&"before".to_string()),
"a name inside a closed, masked token must not read as bare prose: {found:?}"
);
assert!(
found.contains(&"after".to_string()),
"a name inside the unterminated tail is left verbatim, so it reads \
as an ordinary bare mention: {found:?}"
);
}
fn sref(token: &str, name: &str, kind: Option<ItemKind>) -> SiblingRef {
SiblingRef {
token: token.to_string(),
name: name.to_string(),
kind,
}
}
struct SiblingRefCase {
label: &'static str,
content: &'static str,
expected: Vec<SiblingRef>,
}
#[test]
fn sibling_reference_tokens_table() {
let cases = vec![
SiblingRefCase {
label: "a plain {{ns:name}} token, matching expand's literal scan",
content: "hand off to {{ns:dev}}.",
expected: vec![sref("{{ns:dev}}", "dev", None)],
},
SiblingRefCase {
label: "whitespace INSIDE the ns: token is trimmed off the name, \
matching expand's trim of the name",
content: "see {{ns: dev }}",
expected: vec![sref("{{ns: dev }}", "dev", None)],
},
SiblingRefCase {
label: "whitespace BETWEEN the braces and ns: is not a token (M2)",
content: "Hand off to {{ ns:dev }}.",
expected: vec![],
},
SiblingRefCase {
label: "a {{tools:name}} token, matching expand_paths",
content: "run {{tools:build}}",
expected: vec![sref("{{tools:build}}", "build", Some(ItemKind::Tool))],
},
SiblingRefCase {
label: "whitespace between the braces and tools: IS a token, \
matching expand_paths' trim-then-test",
content: "run {{ tools:build }}",
expected: vec![sref("{{ tools:build }}", "build", Some(ItemKind::Tool))],
},
SiblingRefCase {
label: "a kind-qualified {{path:kind:name}} token",
content: "see {{path:skill:dev}}",
expected: vec![sref("{{path:skill:dev}}", "dev", Some(ItemKind::Skill))],
},
SiblingRefCase {
label: "an unqualified {{path:name}} token matches any kind",
content: "see {{path:dev}}",
expected: vec![sref("{{path:dev}}", "dev", None)],
},
SiblingRefCase {
label: "{{path:notakind:name}} treats the whole reference as the name",
content: "see {{path:notakind:name}}",
expected: vec![sref("{{path:notakind:name}}", "notakind:name", None)],
},
SiblingRefCase {
label: "{{self}} is excluded, but the scan resumes after it",
content: "{{self}} then {{ns:dev}}",
expected: vec![sref("{{ns:dev}}", "dev", None)],
},
SiblingRefCase {
label: "an unrecognized token is passed through, and the scan \
resumes just past its opening brace",
content: "{{foo}}{{ns:dev}}",
expected: vec![sref("{{ns:dev}}", "dev", None)],
},
SiblingRefCase {
label: "nested {{{{ns:dev}} is still found by resuming just \
past the opening brace, not past the failed span",
content: "{{{{ns:dev}}",
expected: vec![sref("{{ns:dev}}", "dev", None)],
},
SiblingRefCase {
label: "an unterminated token stops the scan entirely",
content: "{{ns:dev}} {{ns:oops and more text with no closing braces",
expected: vec![sref("{{ns:dev}}", "dev", None)],
},
SiblingRefCase {
label: "duplicate token text is deduped to the first occurrence",
content: "{{ns:dev}} again {{ns:dev}}",
expected: vec![sref("{{ns:dev}}", "dev", None)],
},
SiblingRefCase {
label: "same referent name, different token text: both reported",
content: "{{ns:dev}} {{ns: dev }}",
expected: vec![
sref("{{ns:dev}}", "dev", None),
sref("{{ns: dev }}", "dev", None),
],
},
SiblingRefCase {
label: "an empty {{ns:}} name is still reported, matching \
expand's rejection of an empty referent",
content: "{{ns:}}",
expected: vec![sref("{{ns:}}", "", None)],
},
];
for case in cases {
assert_eq!(
sibling_reference_tokens(case.content),
case.expected,
"case: {}",
case.label
);
}
}
#[test]
fn sibling_reference_tokens_passthrough_resumes_past_the_open_brace_not_the_failed_span() {
assert_eq!(
sibling_reference_tokens("{{{{ns:dev}}"),
vec![sref("{{ns:dev}}", "dev", None)],
"resuming past the failed span instead of past the opening brace \
would miss the nested {{{{ns:dev}} token entirely"
);
}
#[test]
fn sibling_reference_tokens_ns_prefix_test_agrees_with_expand() {
let siblings = sibs(&["dev"]);
let bare = HashSet::new();
let content = "hand off to {{ns:dev}}.";
assert_eq!(
sibling_reference_tokens(content),
vec![sref("{{ns:dev}}", "dev", None)]
);
assert_eq!(
expand(content, &None, &siblings, &bare).unwrap(),
"hand off to dev."
);
let spaced = "hand off to {{ ns:dev }}.";
assert_eq!(sibling_reference_tokens(spaced), vec![]);
assert_eq!(expand(spaced, &None, &siblings, &bare).unwrap(), spaced);
}
#[test]
fn sibling_reference_tokens_tools_prefix_test_agrees_with_expand_paths() {
let sib = [psib(ItemKind::Tool, "build", Some("run.sh"))];
let ctx = PathCtx {
store_root: Path::new("/store"),
home: None,
prefix: &None,
self_kind: ItemKind::Skill,
self_name: "self-item",
siblings: &sib,
};
let content = "run {{ tools:build }}";
assert_eq!(
sibling_reference_tokens(content),
vec![sref("{{ tools:build }}", "build", Some(ItemKind::Tool))]
);
let expanded = expand_paths(content, &ctx).unwrap();
assert!(
expanded.contains("run.sh"),
"expand_paths must still resolve the spaced tools: token: {expanded}"
);
}
}