use crate::asset_snapshot::AssetSnapshot;
use crate::content_graph::ContentGraph;
pub mod asset_class;
pub mod asset_registry;
pub mod block_refs;
pub mod embed_renderer;
pub mod embeds;
pub mod ext_kind;
pub mod folder_class;
pub mod reference;
pub mod fuzzy_path;
pub mod link_class;
pub mod output_url;
pub mod registry;
pub mod title_params;
pub mod wikilink_dispatch;
pub mod md_extract;
#[derive(Debug, Clone)]
pub struct OutgoingLink {
pub target_path: String,
pub display_text: String,
pub link_type: LinkType,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LinkType {
Wikilink,
Embed,
Standard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DiagnosticKind {
MissingAsset,
#[default]
Other,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub message: String,
pub source_path: String,
pub reference: String,
pub kind: DiagnosticKind,
}
#[derive(Debug)]
pub struct ResolveResult {
pub content_markdown: String,
pub outgoing_links: Vec<OutgoingLink>,
pub diagnostics: Vec<Diagnostic>,
pub block_ids: Vec<String>,
pub embed_deps: Vec<(String, String)>,
}
pub fn resolve_content(
source_path: &str,
raw_markdown: &str,
graph: &ContentGraph,
file_reader: &dyn Fn(&str) -> Option<String>,
) -> ResolveResult {
let handlers = embeds::MarkerHandlers::new();
let registry = registry::RendererRegistry::builtin().build();
resolve_content_with_handlers(
source_path,
raw_markdown,
graph,
file_reader,
®istry,
&handlers,
)
}
pub fn resolve_content_with_handlers(
source_path: &str,
raw_markdown: &str,
graph: &ContentGraph,
file_reader: &dyn Fn(&str) -> Option<String>,
registry: ®istry::RendererRegistry,
handlers: &embeds::MarkerHandlers<'_>,
) -> ResolveResult {
let empty_snapshot = AssetSnapshot::new();
resolve_content_with_handlers_and_snapshot(
source_path,
raw_markdown,
graph,
file_reader,
registry,
handlers,
&empty_snapshot,
)
}
pub fn resolve_content_with_handlers_and_snapshot(
source_path: &str,
raw_markdown: &str,
graph: &ContentGraph,
file_reader: &dyn Fn(&str) -> Option<String>,
registry: ®istry::RendererRegistry,
handlers: &embeds::MarkerHandlers<'_>,
_assets: &AssetSnapshot,
) -> ResolveResult {
let (frontmatter, body) = split_frontmatter(raw_markdown);
let outgoing_links: Vec<OutgoingLink> = Vec::new();
let diagnostics: Vec<Diagnostic> = Vec::new();
let _ = registry;
let body = lower_transclusion_and_folder_wikilinks(body, graph, source_path);
let embed_result = embeds::resolve_embeds(&body, source_path, file_reader);
let mut diagnostics = diagnostics;
diagnostics.extend(embed_result.diagnostics);
let embed_deps = embed_result.embed_deps;
let deferred_result = embeds::resolve_deferred_markers(&embed_result.content, handlers);
diagnostics.extend(deferred_result.diagnostics);
let (block_result, block_ids) = block_refs::transform_block_refs(&deferred_result.content);
let content_markdown = match frontmatter {
Some(fm) => {
let resolved_fm = resolve_frontmatter_wikilinks(fm, graph, source_path);
diagnostics.extend(resolved_fm.diagnostics);
format!("{}{}", resolved_fm.content, block_result)
}
None => block_result,
};
ResolveResult {
content_markdown,
outgoing_links,
diagnostics,
block_ids,
embed_deps,
}
}
fn lower_transclusion_and_folder_wikilinks(
body: &str,
graph: &ContentGraph,
source_path: &str,
) -> String {
let mut output_lines: Vec<String> = Vec::with_capacity(body.lines().count() + 1);
let masked = crate::inert_regions::mask_inert(body);
for (line, masked_line) in body.lines().zip(masked.lines()) {
if !masked_line.contains("![[") {
output_lines.push(line.to_string());
continue;
}
let mut rewritten = String::with_capacity(line.len());
let mut rest = line;
while let Some(start) = rest.find("![[") {
let Some((before, from_marker)) = rest.split_at_checked(start) else {
break;
};
rewritten.push_str(before);
rest = from_marker;
let at = line.len() - rest.len();
if masked_line.as_bytes().get(at..at + 3) != Some(b"![[".as_slice()) {
let Some(after) = rest.get(3..) else { break };
rewritten.push_str("![[");
rest = after;
continue;
}
let Some(after) = rest.get(3..) else { break };
let Some(end) = after.find("]]") else { break };
let (Some(inner), Some(token), Some(remainder)) =
(after.get(..end), rest.get(..3 + end + 2), after.get(end + 2..))
else {
break;
};
let inner_no_pothole = match inner.split_once('|') {
Some((f, _)) => f,
None => inner,
};
let (file_part, anchor) = match inner_no_pothole.split_once('#') {
Some((file, anchor)) => (file, Some(anchor)),
None => (inner_no_pothole, None),
};
if file_part.is_empty() {
rewritten.push_str(token);
rest = remainder;
continue;
}
if file_part.ends_with('/') {
let pothole_raw = match inner.split_once('|') {
Some((_, params)) => params,
None => "",
};
let params = embed_renderer::folder_list::parse_params(pothole_raw);
let marker =
embed_renderer::folder_list::emit_marker(file_part, source_path, ¶ms);
rewritten.push_str(&marker);
rest = remainder;
continue;
}
let resolved = fuzzy_path::resolve_reference(file_part, graph, source_path);
let target_path = match resolved {
fuzzy_path::ResolvedRef::Found(p) => p,
fuzzy_path::ResolvedRef::Unresolved => {
rewritten.push_str(token);
rest = remainder;
continue;
}
};
let ext = target_path
.rsplit('.')
.next()
.unwrap_or("")
.to_ascii_lowercase();
if ext == "md" || ext == "markdown" {
let target_with_anchor = match anchor {
Some(a) => format!("{}#{}", target_path, a),
None => target_path,
};
rewritten.push_str("<!-- moss-embed:");
rewritten.push_str(&target_with_anchor);
rewritten.push_str(" -->");
rest = remainder;
continue;
}
let marker_prefix = match ext.as_str() {
"ipynb" => Some("moss-embed-ipynb"),
"csv" | "tsv" => Some("moss-embed-table"),
_ => None,
};
if let Some(prefix) = marker_prefix {
rewritten.push_str("<!-- ");
rewritten.push_str(prefix);
rewritten.push(':');
rewritten.push_str(&target_path);
rewritten.push_str(" -->");
rest = remainder;
continue;
}
rewritten.push_str(token);
rest = remainder;
}
rewritten.push_str(rest);
output_lines.push(rewritten);
}
let mut out = output_lines.join("\n");
if body.ends_with('\n') {
out.push('\n');
}
out
}
pub struct FrontmatterResolveResult {
pub content: String,
pub diagnostics: Vec<Diagnostic>,
}
pub fn resolve_frontmatter_wikilinks(
frontmatter: &str,
graph: &ContentGraph,
source_path: &str,
) -> FrontmatterResolveResult {
let mut diagnostics = Vec::new();
let mut result = String::with_capacity(frontmatter.len());
let bytes = frontmatter.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
let is_embed =
i + 2 < len && bytes[i] == b'!' && bytes[i + 1] == b'[' && bytes[i + 2] == b'[';
let is_wikilink = !is_embed && i + 1 < len && bytes[i] == b'[' && bytes[i + 1] == b'[';
if is_embed || is_wikilink {
let bracket_start = if is_embed { i + 3 } else { i + 2 };
if let Some(close_pos) = find_closing_brackets(bytes, bracket_start) {
#[allow(clippy::string_slice)]
let inner = &frontmatter[bracket_start..close_pos];
let (ref_part, attrs_part) = crate::media::split_pipe(inner);
let resolved_path = match graph.resolve_path(ref_part, source_path) {
Some(mut path) => {
if is_embed && !attrs_part.is_empty() {
path.push('|');
path.push_str(attrs_part);
}
path
}
None => {
diagnostics.push(Diagnostic {
message: format!("Unresolved frontmatter wikilink: [[{}]]", ref_part),
source_path: source_path.to_string(),
reference: ref_part.to_string(),
kind: DiagnosticKind::Other,
});
let mut fallback = ref_part.to_string();
if is_embed && !attrs_part.is_empty() {
fallback.push('|');
fallback.push_str(attrs_part);
}
fallback
}
};
result.push_str(&resolved_path);
i = close_pos + 2; } else {
if is_embed {
result.push_str("![[");
i += 3;
} else {
result.push('[');
i += 1;
}
}
} else {
#[allow(clippy::string_slice)]
let Some(ch) = frontmatter[i..].chars().next() else {
break;
};
result.push(ch);
i += ch.len_utf8();
}
}
FrontmatterResolveResult {
content: result,
diagnostics,
}
}
fn find_closing_brackets(bytes: &[u8], start: usize) -> Option<usize> {
let mut j = start;
while j + 1 < bytes.len() {
if bytes[j] == b']' && bytes[j + 1] == b']' {
return Some(j);
}
j += 1;
}
None
}
fn find_delimiter(content: &str, scan_start: usize) -> Option<usize> {
#[allow(clippy::string_slice)]
let rest = &content[scan_start..];
let mut offset = 0;
for line in rest.lines() {
if line.trim() == "---" {
let close_abs = scan_start + offset + line.len();
return if close_abs < content.len() && content.as_bytes()[close_abs] == b'\n' {
Some(close_abs + 1)
} else {
Some(close_abs)
};
}
offset += line.len() + 1; }
None
}
fn split_frontmatter(content: &str) -> (Option<&str>, &str) {
if content.starts_with("---") {
let after_opening = match content.find('\n') {
Some(pos) => pos + 1,
None => return (None, content),
};
#[allow(clippy::string_slice)]
match find_delimiter(content, after_opening) {
Some(split_pos) => (Some(&content[..split_pos]), &content[split_pos..]),
None => (None, content), }
} else {
#[allow(clippy::string_slice)]
match crate::frontmatter_typed::simplified_frontmatter_delimiter(content)
.and_then(|line_start| find_delimiter(content, line_start))
{
Some(split_pos) => (Some(&content[..split_pos]), &content[split_pos..]),
None => (None, content), }
}
}
pub(crate) fn parent_dir(path: &str) -> &str {
match path.rfind('/') {
#[allow(clippy::string_slice)]
Some(pos) => &path[..pos],
None => "",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content_graph::ContentGraphBuilder;
use std::collections::HashMap;
fn test_graph() -> ContentGraph {
let mut b = ContentGraphBuilder::new();
b.add_file("guide.md", "guide");
b.add_file("note.md", "note");
b.add_file("disclaimer.md", "disclaimer");
b.add_file("assets/photo.jpg", "photo");
b.build()
}
fn test_files() -> HashMap<String, String> {
let mut files = HashMap::new();
files.insert(
"disclaimer.md".into(),
"---\ntitle: Disclaimer\n---\nThis is the disclaimer.\n\nSee [[guide]] for details."
.into(),
);
files
}
fn mock_reader(files: &HashMap<String, String>) -> impl Fn(&str) -> Option<String> + '_ {
move |path: &str| files.get(path).cloned()
}
fn lower(body: &str) -> String {
lower_transclusion_and_folder_wikilinks(body, &test_graph(), "index.md")
}
#[test]
fn transclusion_lowers_to_a_marker() {
assert_eq!(lower("![[note.md]]\n"), "<!-- moss-embed:note.md -->\n");
}
#[test]
fn transclusion_in_a_code_fence_is_left_alone() {
let md = "```\n![[note.md]]\n```\n";
assert_eq!(lower(md), md);
}
#[test]
fn transclusion_in_an_indented_code_block_is_left_alone() {
let md = "how to embed:\n\n ![[note.md]]\n";
assert_eq!(lower(md), md);
}
#[test]
fn transclusion_in_an_inline_code_span_is_left_alone() {
let md = "write `![[note.md]]` to transclude a note\n";
assert_eq!(lower(md), md);
}
#[test]
fn transclusion_in_an_html_comment_is_left_alone() {
let md = "<!-- TODO: ![[note.md]] -->\n";
assert_eq!(lower(md), md);
}
#[test]
fn a_live_transclusion_beside_an_inert_one_still_lowers() {
assert_eq!(
lower("`![[note.md]]` renders ![[note.md]] inline\n"),
"`![[note.md]]` renders <!-- moss-embed:note.md --> inline\n"
);
}
#[test]
fn folder_list_embed_in_a_comment_is_left_alone() {
let md = "<!-- ![[/posts/|limit:3]] -->\n";
assert_eq!(lower(md), md);
}
#[test]
fn test_split_fm_present() {
let input = "---\ntitle: Hello\n---\nBody here.";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("---\ntitle: Hello\n---\n"));
assert_eq!(body, "Body here.");
}
#[test]
fn test_split_fm_absent() {
let input = "Just body content.";
let (fm, body) = split_frontmatter(input);
assert!(fm.is_none());
assert_eq!(body, input);
}
#[test]
fn test_split_fm_no_closing() {
let input = "---\ntitle: Hello\nno closing delimiter";
let (fm, body) = split_frontmatter(input);
assert!(fm.is_none());
assert_eq!(body, input);
}
#[test]
fn test_split_simplified_frontmatter() {
let input = "sidebar: [[news]]\n---\n\n# Hello";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("sidebar: [[news]]\n---\n"));
assert_eq!(body, "\n# Hello");
}
#[test]
fn test_split_simplified_preserves_body() {
let input = "children: false\nuid: a48746ca\n---\n\n# Page Title\n\nBody content here\n";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("children: false\nuid: a48746ca\n---\n"));
assert_eq!(body, "\n# Page Title\n\nBody content here\n");
}
#[test]
fn test_split_does_not_treat_a_grid_cell_separator_as_frontmatter() {
let input = ":::grid 2\n### [[alpha]]\n\n---\n\n### [[beta]]\n:::\n";
let (fm, body) = split_frontmatter(input);
assert!(fm.is_none(), "grid cell separator is not a delimiter; got fm={:?}", fm);
assert_eq!(body, input);
}
#[test]
fn test_split_does_not_treat_a_fenced_dash_line_as_frontmatter() {
let input = "Intro.\n\n```yaml\ntitle: Example\n---\n```\n\nMore prose.\n";
let (fm, body) = split_frontmatter(input);
assert!(fm.is_none(), "fenced `---` is a code sample; got fm={:?}", fm);
assert_eq!(body, input);
}
#[test]
fn test_split_finds_frontmatter_that_precedes_a_directive_block() {
let input = "children: false\n---\n\n:::grid 2\nA\n\n---\n\nB\n:::\n";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("children: false\n---\n"));
assert_eq!(body, "\n:::grid 2\nA\n\n---\n\nB\n:::\n");
}
#[test]
fn test_split_no_delimiter() {
let input = "Just some content\nwith multiple lines\nbut no delimiter";
let (fm, body) = split_frontmatter(input);
assert!(fm.is_none());
assert_eq!(body, input);
}
#[test]
fn test_split_simplified_with_quoted_wikilink() {
let input = "sidebar: \"[[news]]\"\n---\nBody text";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("sidebar: \"[[news]]\"\n---\n"));
assert_eq!(body, "Body text");
}
#[test]
fn test_split_simplified_empty_body() {
let input = "title: Test\n---\n";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("title: Test\n---\n"));
assert_eq!(body, "");
}
#[test]
fn test_split_simplified_delimiter_at_eof_no_newline() {
let input = "title: Test\n---";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("title: Test\n---"));
assert_eq!(body, "");
}
#[test]
fn test_split_simplified_multiple_dashes_in_body() {
let input = "title: Test\n---\n\nSome body\n---\nMore body";
let (fm, body) = split_frontmatter(input);
assert_eq!(fm, Some("title: Test\n---\n"));
assert_eq!(body, "\nSome body\n---\nMore body");
}
#[test]
fn test_full_resolve_pipeline() {
let graph = test_graph();
let files = test_files();
let input = "---\ntitle: Test\n---\nSee [[guide#Setup]] for help.\n\nImportant point. ^my-block\n\n> [!warning] Watch Out\n> Be careful here.";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
assert!(result
.content_markdown
.starts_with("---\ntitle: Test\n---\n"));
assert!(result.content_markdown.contains("[[guide#Setup]]"));
assert!(result
.content_markdown
.contains("<span id=\"my-block\"></span>"));
assert_eq!(result.block_ids, vec!["my-block"]);
assert!(
result.content_markdown.contains("> [!warning] Watch Out"),
"Expected callout markdown to pass through verbatim post-PR7a, got: {}",
result.content_markdown
);
}
#[test]
fn test_frontmatter_preserved() {
let graph = test_graph();
let files = HashMap::new();
let input = "---\ntitle: My Page\ntags:\n - rust\n - wasm\n---\nPlain body.";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
assert!(result
.content_markdown
.starts_with("---\ntitle: My Page\ntags:\n - rust\n - wasm\n---\n"));
assert!(result.content_markdown.ends_with("Plain body."));
}
#[test]
fn test_no_obsidian_syntax() {
let graph = test_graph();
let files = HashMap::new();
let input = "---\ntitle: Plain\n---\nJust a plain paragraph.\n\nAnother paragraph.";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
assert_eq!(result.content_markdown, input);
assert!(result.outgoing_links.is_empty());
assert!(result.diagnostics.is_empty());
assert!(result.block_ids.is_empty());
assert!(result.embed_deps.is_empty());
}
#[test]
fn test_embedded_wikilinks_resolved() {
let graph = test_graph();
let files = test_files();
let input = "![[disclaimer]]";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
assert!(
result.content_markdown.contains("[[guide]]"),
"Expected raw wikilink from embedded content, got: {}",
result.content_markdown
);
assert!(result.content_markdown.contains("This is the disclaimer."));
}
#[test]
fn test_diagnostics_merged() {
let graph = test_graph();
let files = HashMap::new();
let input = "[[nonexistent]] and ![[missing]]";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
assert!(
result.diagnostics.is_empty(),
"Expected zero diagnostics post-PR2 (body wikilinks deferred), got: {:?}",
result.diagnostics
);
assert!(result.content_markdown.contains("[[nonexistent]]"));
assert!(result.content_markdown.contains("![[missing]]"));
}
#[test]
fn test_outgoing_links_tracked() {
let graph = test_graph();
let files = test_files();
let input = "[[guide]]\n\n![[disclaimer]]";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
let wikilinks: Vec<_> = result
.outgoing_links
.iter()
.filter(|l| l.link_type == LinkType::Wikilink)
.collect();
let embeds: Vec<_> = result
.outgoing_links
.iter()
.filter(|l| l.link_type == LinkType::Embed)
.collect();
assert!(
wikilinks.is_empty(),
"Expected zero wikilink outgoing links from resolve_content post-PR2; got {}: {:?}",
wikilinks.len(),
wikilinks
);
let _ = embeds; assert!(
!result.embed_deps.is_empty(),
"Expected at least 1 embed outgoing link"
);
}
#[test]
fn test_embed_deps_tracked() {
let graph = test_graph();
let files = test_files();
let input = "![[disclaimer]]";
let result = resolve_content("note.md", input, &graph, &mock_reader(&files));
assert!(
result
.embed_deps
.contains(&("disclaimer.md".to_string(), "note.md".to_string())),
"Expected embed dep (disclaimer.md, note.md), got: {:?}",
result.embed_deps
);
}
#[test]
fn test_deeply_nested_unicode_bare_filename() {
let mut b = ContentGraphBuilder::new();
b.add_file(
"assets/d9512f2d-fdcf-4a22-b1d5-340f74ddedae.jpg",
"d9512f2d",
);
b.add_file(
"articles/\u{65e0}\u{7528}\u{4e4b}\u{65c5}/\u{771f}\u{6b63}\u{7684}\u{65c5}\u{7a0b}.md",
"articles/\u{65e0}\u{7528}\u{4e4b}\u{65c5}/\u{771f}\u{6b63}\u{7684}\u{65c5}\u{7a0b}",
);
let graph = b.build();
let files = HashMap::new();
let input = "---\ndate: 2025-12-03\n---\n\n\nSome text.";
let result = resolve_content(
"articles/\u{65e0}\u{7528}\u{4e4b}\u{65c5}/\u{771f}\u{6b63}\u{7684}\u{65c5}\u{7a0b}.md",
input,
&graph,
&mock_reader(&files),
);
assert!(
result
.content_markdown
.contains(""),
"Expected bare filename to pass through verbatim, got: {}",
result.content_markdown
);
}
#[test]
fn test_bare_filename_image_passes_through_in_pipeline() {
let mut b = ContentGraphBuilder::new();
b.add_file("guide.md", "guide");
b.add_file("note.md", "note");
b.add_file("assets/photo.jpg", "photo");
let graph = b.build();
let files = HashMap::new();
let input = "---\ntitle: Test\n---\n\n\nSome text.";
let result = resolve_content("articles/post.md", input, &graph, &mock_reader(&files));
assert!(result
.content_markdown
.starts_with("---\ntitle: Test\n---\n"));
assert!(
result.content_markdown.contains(""),
"Expected bare filename to pass through verbatim, got: {}",
result.content_markdown
);
let standard_links: Vec<_> = result
.outgoing_links
.iter()
.filter(|l| l.link_type == LinkType::Standard)
.collect();
assert!(
standard_links.is_empty(),
"Expected zero standard outgoing links from resolve_content post-PR7a, got: {:?}",
standard_links
);
}
fn fm_test_graph() -> ContentGraph {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("news.md", "news");
b.add_file("news/index.md", "news-index");
b.add_file("assets/photo.jpg", "photo");
b.add_file("posts/ch-1.md", "ch-1");
b.add_file("posts/ch-2.md", "ch-2");
b.build()
}
#[test]
fn test_fm_wikilink_basic_quoted() {
let graph = fm_test_graph();
let fm = "---\nsidebar: \"[[news]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\nsidebar: \"news.md\"\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_unquoted() {
let graph = fm_test_graph();
let fm = "---\nsidebar: [[news]]\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\nsidebar: news.md\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_cover_image() {
let graph = fm_test_graph();
let fm = "---\ncover: \"[[photo.jpg]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\ncover: \"assets/photo.jpg\"\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_folder_note() {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("news/index.md", "news-index");
let graph = b.build();
let fm = "---\nsidebar: \"[[news]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\nsidebar: \"news/index.md\"\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_array_items() {
let graph = fm_test_graph();
let fm = "---\nseries: [\"[[ch-1]]\", \"[[ch-2]]\"]\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(
result.content,
"---\nseries: [\"posts/ch-1.md\", \"posts/ch-2.md\"]\n---\n"
);
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_unresolved() {
let graph = fm_test_graph();
let fm = "---\nsidebar: \"[[missing]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\nsidebar: \"missing\"\n---\n");
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].reference, "missing");
assert_eq!(result.diagnostics[0].source_path, "index.md");
assert!(result.diagnostics[0].message.contains("[[missing]]"));
}
#[test]
fn test_fm_wikilink_multiple() {
let graph = fm_test_graph();
let fm = "---\nsidebar: \"[[news]]\"\ncover: \"[[photo.jpg]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(
result.content,
"---\nsidebar: \"news.md\"\ncover: \"assets/photo.jpg\"\n---\n"
);
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_no_wikilinks() {
let graph = fm_test_graph();
let fm = "---\ntitle: Hello\ntags:\n - rust\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, fm);
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_simplified_frontmatter_wikilink() {
let graph = fm_test_graph();
let fm = "sidebar: \"[[news]]\"\nchildren: false\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(
result.content,
"sidebar: \"news.md\"\nchildren: false\n---\n"
);
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_unclosed_wikilink_preserved() {
let graph = fm_test_graph();
let fm = "---\nsidebar: \"[[unclosed\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\nsidebar: \"[[unclosed\"\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_mixed_resolved_and_unresolved() {
let graph = fm_test_graph();
let fm = "---\nsidebar: \"[[news]]\"\nrelated: \"[[missing]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(
result.content,
"---\nsidebar: \"news.md\"\nrelated: \"missing\"\n---\n"
);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].reference, "missing");
}
#[test]
fn test_fm_wikilink_alias_discarded() {
let graph = fm_test_graph();
let fm = "---\ncover: \"[[photo.jpg|left]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\ncover: \"assets/photo.jpg\"\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_embed_wikilink_with_attrs() {
let graph = fm_test_graph();
let fm = "---\ncover: \"![[photo.jpg|cover left]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(
result.content,
"---\ncover: \"assets/photo.jpg|cover left\"\n---\n"
);
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_no_attrs_unchanged() {
let graph = fm_test_graph();
let fm = "---\ncover: \"[[photo.jpg]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\ncover: \"assets/photo.jpg\"\n---\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_fm_wikilink_alias_unresolved_discarded() {
let graph = fm_test_graph();
let fm = "---\ncover: \"[[missing.jpg|left]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(result.content, "---\ncover: \"missing.jpg\"\n---\n");
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].reference, "missing.jpg");
}
#[test]
fn test_fm_embed_wikilink_with_fit_and_position() {
let graph = fm_test_graph();
let fm = "---\ncover: \"![[photo.jpg|contain top-right]]\"\n---\n";
let result = resolve_frontmatter_wikilinks(fm, &graph, "index.md");
assert_eq!(
result.content,
"---\ncover: \"assets/photo.jpg|contain top-right\"\n---\n"
);
assert!(result.diagnostics.is_empty());
}
#[test]
fn test_simplified_frontmatter_wikilink_resolved_to_path() {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("news.md", "news");
let graph = b.build();
let files = HashMap::new();
let input = "children: false\nsidebar: \"[[news]]\"\nuid: a48746ca\n---\n\n# Welcome\n\nBody with [[news]] link.";
let result = resolve_content("index.md", input, &graph, &mock_reader(&files));
assert!(
result
.content_markdown
.starts_with("children: false\nsidebar: \"news.md\"\nuid: a48746ca\n---\n"),
"Frontmatter wikilink not resolved to path: {}",
result.content_markdown
);
assert!(
result.content_markdown.contains("[[news]]"),
"Expected body wikilink to pass through verbatim, got: {}",
result.content_markdown
);
}
#[test]
fn test_frontmatter_embed_wikilink_stripped() {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("photos/hero.jpg", "hero");
let graph = b.build();
let files = HashMap::new();
let input = "cover: \"![[hero.jpg]]\"\n---\n\n# Page";
let result = resolve_content("index.md", input, &graph, &mock_reader(&files));
assert!(
result
.content_markdown
.starts_with("cover: \"photos/hero.jpg\"\n---"),
"Embed wikilink ! prefix not stripped: {}",
result.content_markdown
);
}
#[test]
fn test_frontmatter_embed_wikilink_with_attrs() {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("photos/hero.jpg", "hero");
let graph = b.build();
let files = HashMap::new();
let input = "cover: \"![[hero.jpg|cover left]]\"\n---\n\n# Page";
let result = resolve_content("index.md", input, &graph, &mock_reader(&files));
assert!(
result
.content_markdown
.starts_with("cover: \"photos/hero.jpg|cover left\"\n---"),
"Embed wikilink with attrs not resolved correctly: {}",
result.content_markdown
);
}
#[test]
fn standard_markdown_link_passes_through_in_pipeline() {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("文字/文字.md", "writings");
let graph = b.build();
let files = HashMap::new();
let result = resolve_content(
"index.md",
"[文字](文字.md)\n",
&graph,
&mock_reader(&files),
);
assert!(
result.content_markdown.contains("[文字](文字.md)"),
"expected verbatim pass-through, got: {}",
result.content_markdown
);
}
#[test]
fn test_frontmatter_link_wikilink_alias_discarded() {
let mut b = ContentGraphBuilder::new();
b.add_file("index.md", "index");
b.add_file("photos/hero.jpg", "hero");
let graph = b.build();
let files = HashMap::new();
let input = "cover: \"[[hero.jpg|My Hero]]\"\n---\n\n# Page";
let result = resolve_content("index.md", input, &graph, &mock_reader(&files));
assert!(
result
.content_markdown
.starts_with("cover: \"photos/hero.jpg\"\n---"),
"Link wikilink alias should be discarded, got: {}",
result.content_markdown
);
}
}