//! End-to-end tests for the HTML renderer.
//!
//! Pipeline: source text → `VimwikiSyntax::parse` → `HtmlRenderer::render`.
use nuwiki_core::ast::LinkTarget;
use nuwiki_core::render::{HtmlRenderer, Renderer};
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
use nuwiki_core::syntax::SyntaxPlugin;
fn render(src: &str) -> String {
let doc = VimwikiSyntax::new().parse(src);
HtmlRenderer::new().render_to_string(&doc).unwrap()
}
// ===== Block elements =====
#[test]
fn empty_document_renders_to_empty_string() {
assert_eq!(render(""), "");
}
#[test]
fn heading_levels() {
for level in 1..=6 {
let bars = "=".repeat(level);
let out = render(&format!("{bars} Title {bars}\n"));
assert!(out.starts_with(&format!("Title ")));
}
}
#[test]
fn centered_heading_gets_centered_class() {
let out = render(" = T =\n");
assert!(out.contains("
T "));
}
#[test]
fn paragraph_wraps_inline_content() {
let out = render("Hello world\n");
assert_eq!(out.trim(), "Hello world
");
}
#[test]
fn horizontal_rule() {
let out = render("----\n");
assert!(out.contains(" "));
}
#[test]
fn blockquote_lines_become_paragraphs() {
let out = render("> first\n> second\n");
assert!(out.contains(""));
assert!(out.contains("first
"));
assert!(out.contains("second
"));
}
#[test]
fn preformatted_block_emits_pre_code_with_language_class() {
let out = render("{{{rust\nfn main() {}\n}}}\n");
assert!(out.contains(""));
assert!(out.contains("fn main() {}"));
assert!(out.contains(" "));
}
#[test]
fn math_block_emits_div() {
let out = render("{{$\nx=1\n}}$\n");
assert!(out.contains(""));
assert!(out.contains("x=1"));
}
#[test]
fn unordered_list_with_checkbox() {
let out = render("- [X] done\n- not done\n");
assert!(out.contains("
"));
assert!(out.contains(" "));
assert!(out.contains("done"));
assert!(out.contains("not done"));
}
#[test]
fn ordered_list() {
let out = render("1. first\n");
assert!(out.contains(""));
assert!(out.contains("first "));
}
#[test]
fn nested_list_renders_recursively() {
let out = render("- top\n - nested\n");
assert!(out.contains(""));
let inner_idx = out
.find("\ntop")
.map(|i| i + "\ntop".len())
.unwrap_or(0);
assert!(out[inner_idx..].contains(""));
assert!(out.contains("nested"));
}
#[test]
fn definition_list() {
let out = render("Term:: Defn\n");
assert!(out.contains(""));
assert!(out.contains("Term "));
assert!(out.contains("Defn "));
}
#[test]
fn table_with_header_separator() {
let out = render("| a | b |\n|---|---|\n| 1 | 2 |\n");
assert!(out.contains(""));
assert!(out.contains(""));
assert!(out.contains(" a "));
assert!(out.contains(" "));
assert!(out.contains(" 1 "));
}
#[test]
fn single_comment_becomes_html_comment() {
let out = render("%% hidden\n");
assert!(out.contains(""));
}
// ===== Inline =====
#[test]
fn bold_italic_strike_super_sub() {
let out = render("*b* _i_ ~~s~~ ^sup^ ,,sub,,\n");
assert!(out.contains("b "));
assert!(out.contains("i "));
assert!(out.contains("s"));
assert!(out.contains("sup "));
assert!(out.contains("sub "));
}
#[test]
fn inline_code_is_escaped() {
let out = render("Use `Vec` here\n");
assert!(out.contains("Vec<u32>"));
}
#[test]
fn keyword_spans() {
let out = render("TODO write tests\n");
assert!(out.contains("TODO "));
}
#[test]
fn raw_url_link() {
let out = render("See https://example.com here\n");
assert!(out.contains("https://example.com "));
}
#[test]
fn wikilink_with_default_resolver() {
let out = render("[[Page]]\n");
assert!(out.contains("Page "));
}
#[test]
fn wikilink_with_anchor_default_resolver() {
let out = render("[[Page#Section]]\n");
assert!(out.contains(""));
}
#[test]
fn anchor_only_link() {
let out = render("[[#Section]]\n");
assert!(out.contains(" "));
}
#[test]
fn interwiki_numbered_link() {
let out = render("[[wiki1:Page]]\n");
assert!(out.contains(" "));
}
#[test]
fn interwiki_named_link() {
let out = render("[[wn.Notes:Page]]\n");
assert!(out.contains(" "));
}
#[test]
fn diary_link() {
let out = render("[[diary:2026-05-10]]\n");
assert!(out.contains(" "));
}
#[test]
fn external_link_routes_to_external_link_node() {
let out = render("[[https://example.com|docs]]\n");
assert!(out.contains(" docs "));
}
#[test]
fn transclusion_becomes_img() {
let out = render("{{img.png|cat photo|class=hi}}\n");
assert!(out.contains(" c & d \"e\"\n");
assert!(out.contains("a < b > c & d "e""));
}
// ===== Custom link resolver =====
#[test]
fn custom_link_resolver_overrides_href() {
let doc = VimwikiSyntax::new().parse("[[Page]]\n");
let renderer = HtmlRenderer::new()
.with_link_resolver(|t: &LinkTarget| format!("/wiki/{}", t.path.as_deref().unwrap_or("")));
let out = renderer.render_to_string(&doc).unwrap();
assert!(out.contains(""));
}
// ===== Template =====
#[test]
fn template_substitutes_content_and_title() {
let doc = VimwikiSyntax::new().parse("%title My Page\n= Hello =\n");
let renderer = HtmlRenderer::new().with_template(
"{{title}} \
{{content}}",
);
let out = renderer.render_to_string(&doc).unwrap();
assert!(out.contains("My Page "));
assert!(out.contains("Hello "));
}
#[test]
fn template_with_missing_title_substitutes_empty_string() {
let doc = VimwikiSyntax::new().parse("= Heading =\n");
let renderer = HtmlRenderer::new().with_template("{{title}} {{content}}");
let out = renderer.render_to_string(&doc).unwrap();
assert!(out.contains(" "));
assert!(out.contains("Heading "));
}
// ===== End-to-end smoke =====
#[test]
fn full_document_round_trip() {
let src = "\
%title Smoke
= Heading =
Paragraph with *bold* and _italic_ and `code`.
- one
- two
> quoted
----
| a | b |
|---|---|
| 1 | 2 |
{{{rust
fn x() {}
}}}
";
let doc = VimwikiSyntax::new().parse(src);
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
// A few sanity checks; the full output is exercised piece-by-piece above.
for needle in [
"Heading ",
"bold ",
"italic ",
"code",
"",
"",
" ",
"",
"",
] {
assert!(
out.contains(needle),
"expected {needle:?} in output:\n{out}"
);
}
}