diff --git a/crates/nuwiki-core/src/ast/block.rs b/crates/nuwiki-core/src/ast/block.rs index 52ccf8d..fb59e98 100644 --- a/crates/nuwiki-core/src/ast/block.rs +++ b/crates/nuwiki-core/src/ast/block.rs @@ -53,6 +53,10 @@ pub struct PreformattedNode { pub span: Span, pub content: String, pub language: Option, + /// Verbatim text after the opening `{{{`, trailing whitespace trimmed + /// (e.g. `python` or `class="brush: python"`). vimwiki inserts this as + /// raw attributes on the `
` tag, so we preserve it for byte-parity.
+    pub attrs: String,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
diff --git a/crates/nuwiki-core/src/render/html.rs b/crates/nuwiki-core/src/render/html.rs
index 3d2558b..cbb628c 100644
--- a/crates/nuwiki-core/src/render/html.rs
+++ b/crates/nuwiki-core/src/render/html.rs
@@ -278,10 +278,26 @@ impl HtmlRenderer {
         // quotes) go through `render_block` and are left unnumbered, matching
         // upstream which only numbers document-level headers.
         let mut numberer = HeadingNumberer::new(self.header_numbering, &self.header_numbering_sym);
+        // Stack of (level, anchor) for the headings enclosing the current one,
+        // so each heading can carry vimwiki's hierarchical id (the parent
+        // anchors joined by `-`, e.g. `Title-Section`).
+        let mut ancestors: Vec<(u8, String)> = Vec::new();
         for block in &doc.children {
             if let BlockNode::Heading(n) = block {
-                let number = numberer.prefix(n.level.clamp(1, 6));
-                self.render_heading(n, &number, w)?;
+                let level = n.level.clamp(1, 6);
+                let number = numberer.prefix(level);
+                let anchor = crate::ast::inline_text(&n.children);
+                while ancestors.last().is_some_and(|(l, _)| *l >= level) {
+                    ancestors.pop();
+                }
+                let mut hier = String::new();
+                for (_, a) in &ancestors {
+                    hier.push_str(a);
+                    hier.push('-');
+                }
+                hier.push_str(&anchor);
+                ancestors.push((level, anchor));
+                self.render_heading(n, &number, &hier, w)?;
             } else {
                 self.render_block(block, w)?;
             }
@@ -291,7 +307,7 @@ impl HtmlRenderer {
 
     fn render_block(&self, block: &BlockNode, w: &mut dyn Write) -> io::Result<()> {
         match block {
-            BlockNode::Heading(n) => self.render_heading(n, "", w),
+            BlockNode::Heading(n) => self.render_heading(n, "", "", w),
             BlockNode::Paragraph(n) => self.render_paragraph(n, w),
             BlockNode::HorizontalRule(n) => self.render_hr(n, w),
             BlockNode::Blockquote(n) => self.render_blockquote(n, w),
@@ -307,13 +323,14 @@ impl HtmlRenderer {
     }
 
     fn render_tag(&self, n: &TagNode, w: &mut dyn Write) -> io::Result<()> {
-        // `id="tag-…"` lets `[[Page#tag-name]]` jump to the rendered tag.
+        // `id="…"` (bare tag name, matching vimwiki) lets `[[Page#name]]` jump
+        // to the rendered tag.
         w.write_all(b"
")?; for (i, name) in n.tags.iter().enumerate() { if i > 0 { w.write_all(b" ")?; } - w.write_all(b"")?; write_escaped(name, w)?; @@ -324,34 +341,65 @@ impl HtmlRenderer { /// Render a heading. `number` is the optional section-number prefix /// (already including its trailing symbol and space, e.g. `"1.2. "`); - /// pass `""` for no numbering. - fn render_heading(&self, n: &HeadingNode, number: &str, w: &mut dyn Write) -> io::Result<()> { + /// pass `""` for no numbering. `hier_id` is the hierarchical anchor id + /// (parent anchors joined by `-`); pass `""` to default it to the flat id. + fn render_heading( + &self, + n: &HeadingNode, + number: &str, + hier_id: &str, + w: &mut dyn Write, + ) -> io::Result<()> { let level = n.level.clamp(1, 6); let anchor = crate::ast::inline_text(&n.children); + // The TOC section heading (vimwiki `toc_header`) is wrapped in a - // `
`, matching upstream's HTML so vimwiki's stylesheet - // (`.toc { … }`) applies. The class goes on the wrapping div, not the - // heading element. + // `
`, matching upstream so vimwiki's `.toc { … }` + // stylesheet rules apply. Kept in its simple form. let is_toc = !self.toc_header.is_empty() && anchor.eq_ignore_ascii_case(&self.toc_header); if is_toc { w.write_all(b"
")?; + write!(w, "")?; + if !number.is_empty() { + write_escaped(number, w)?; + } + self.render_inlines(&n.children, w)?; + write!(w, "
")?; + return writeln!(w); } - let class = if n.centered { - " class=\"centered\"" - } else { - "" - }; - write!(w, "")?; + if !number.is_empty() { + write_escaped(number, w)?; + } + self.render_inlines(&n.children, w)?; + write!(w, "")?; + return writeln!(w); + } + + // Regular heading: vimwiki's structure — a wrapping `
` carrying the + // hierarchical id, `class="header"` on the heading, and an in-heading + // self-anchor linking to that id. The heading keeps the flat id so + // existing intra-page TOC / `#anchor` links still resolve. + let hier = if hier_id.is_empty() { &anchor } else { hier_id }; + w.write_all(b"")?; - } + write!(w, "
")?; writeln!(w) } @@ -379,16 +427,16 @@ impl HtmlRenderer { } fn render_preformatted(&self, n: &PreformattedNode, w: &mut dyn Write) -> io::Result<()> { - match &n.language { - Some(lang) => { - w.write_all(b"
")?;
-            }
-            None => w.write_all(b"
")?,
+        // vimwiki drops the verbatim fence text into the `
` tag as raw
+        // attributes (`{{{python` → `
`), with no inner ``.
+        // We mirror that so highlighters wired for vimwiki output still apply.
+        if n.attrs.is_empty() {
+            w.write_all(b"
")?;
+        } else {
+            write!(w, "
", n.attrs)?;
         }
         write_escaped(&n.content, w)?;
-        w.write_all(b"
\n") + w.write_all(b"
\n") } fn render_math_block(&self, n: &MathBlockNode, w: &mut dyn Write) -> io::Result<()> { diff --git a/crates/nuwiki-core/src/syntax/vimwiki/lexer.rs b/crates/nuwiki-core/src/syntax/vimwiki/lexer.rs index c8b120d..e0a6aa5 100644 --- a/crates/nuwiki-core/src/syntax/vimwiki/lexer.rs +++ b/crates/nuwiki-core/src/syntax/vimwiki/lexer.rs @@ -81,6 +81,9 @@ pub enum VimwikiTokenKind { PreformattedOpen { language: Option, attrs: HashMap, + /// Verbatim text after `{{{` (trailing whitespace trimmed), preserved + /// so the HTML renderer can reproduce vimwiki's raw `
` attrs.
+        raw: String,
     },
     PreformattedClose,
     PreformattedLine(String),
@@ -421,7 +424,14 @@ impl<'src> LexState<'src> {
         // pairs separated by spaces. Keep parsing forgiving.
         let after = trimmed[3..].trim();
         let (language, attrs) = parse_fence_attrs(after);
-        let kind = VimwikiTokenKind::PreformattedOpen { language, attrs };
+        // vimwiki keeps everything after `{{{` (minus trailing whitespace) and
+        // drops it verbatim into the `
` tag, so capture it unparsed.
+        let raw = trimmed[3..].trim_end().to_string();
+        let kind = VimwikiTokenKind::PreformattedOpen {
+            language,
+            attrs,
+            raw,
+        };
         let span = Span::new(self.pos_in_line(indent), self.line_end_pos(line));
         self.push(kind, span);
         self.mode = BlockMode::Preformatted;
diff --git a/crates/nuwiki-core/src/syntax/vimwiki/parser.rs b/crates/nuwiki-core/src/syntax/vimwiki/parser.rs
index e0e0c0b..89f2ea1 100644
--- a/crates/nuwiki-core/src/syntax/vimwiki/parser.rs
+++ b/crates/nuwiki-core/src/syntax/vimwiki/parser.rs
@@ -374,8 +374,8 @@ impl<'a> ParseState<'a> {
         let open = self.advance().unwrap();
         let span_start = open.span.start;
         let mut span_end = open.span.end;
-        let language = match &open.kind {
-            K::PreformattedOpen { language, .. } => language.clone(),
+        let (language, attrs) = match &open.kind {
+            K::PreformattedOpen { language, raw, .. } => (language.clone(), raw.clone()),
             _ => unreachable!(),
         };
         let mut content = String::new();
@@ -390,6 +390,7 @@ impl<'a> ParseState<'a> {
                         span: Span::new(span_start, span_end),
                         content,
                         language,
+                        attrs,
                     });
                 }
                 K::PreformattedLine(s) => {
@@ -413,6 +414,7 @@ impl<'a> ParseState<'a> {
             span: Span::new(span_start, span_end),
             content,
             language,
+            attrs,
         })
     }
 
diff --git a/crates/nuwiki-core/tests/html_renderer.rs b/crates/nuwiki-core/tests/html_renderer.rs
index 1ead749..915742e 100644
--- a/crates/nuwiki-core/tests/html_renderer.rs
+++ b/crates/nuwiki-core/tests/html_renderer.rs
@@ -63,7 +63,11 @@ 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")));
+        // vimwiki structure: 
+ //
. A lone heading has hier == flat. + assert!(out.starts_with(&format!( + "" + )), "got: {out}"); } } @@ -94,11 +98,26 @@ fn blockquote_lines_become_paragraphs() { } #[test] -fn preformatted_block_emits_pre_code_with_language_class() { +fn preformatted_block_emits_pre_with_raw_fence_attrs() { + // vimwiki drops the verbatim fence text into the
 tag (`{{{rust` →
+    // `
`), with no inner  wrapper.
     let out = render("{{{rust\nfn main() {}\n}}}\n");
-    assert!(out.contains("
"));
+    assert!(out.contains("
"), "got: {out}");
     assert!(out.contains("fn main() {}"));
-    assert!(out.contains("
")); + assert!(out.contains("
")); + assert!(!out.contains("plain"), "got: {out}"); +} + +#[test] +fn preformatted_block_keeps_full_attr_string() { + let out = render("{{{class=\"brush: python\"\nx\n}}}\n"); + assert!(out.contains("
"), "got: {out}");
 }
 
 #[test]
@@ -244,7 +263,10 @@ fn heading_id_matches_anchor_link_href() {
     // for "exported HTML anchor links don't work".
     let out = render("= My Section =\n\n[[#My Section]]\n");
     assert!(
-        out.contains("

My Section

"), + out.contains( + "" + ), "heading id: {out}" ); assert!( @@ -276,7 +298,13 @@ fn non_toc_heading_not_wrapped() { .with_toc_header("Contents") .render_to_string(&doc) .unwrap(); - assert!(out.contains("

Intro

"), "got: {out}"); + assert!( + out.contains( + "

\ + Intro

" + ), + "got: {out}" + ); assert!(!out.contains("class=\"toc\""), "got: {out}"); } @@ -342,7 +370,7 @@ fn template_substitutes_content_and_title() { ); let out = renderer.render_to_string(&doc).unwrap(); assert!(out.contains("My Page")); - assert!(out.contains("

Hello

")); + assert!(out.contains("")); } #[test] @@ -351,7 +379,7 @@ fn template_with_missing_title_substitutes_empty_string() { let renderer = HtmlRenderer::new().with_template("{{title}}{{content}}"); let out = renderer.render_to_string(&doc).unwrap(); assert!(out.contains("")); - assert!(out.contains("

Heading

")); + assert!(out.contains("")); } #[test] @@ -367,7 +395,7 @@ fn template_substitutes_vimwiki_percent_placeholders() { let out = renderer.render_to_string(&doc).unwrap(); assert!(out.contains("My Page"), "title: {out}"); assert!( - out.contains("

Hello

"), + out.contains(""), "content: {out}" ); assert!(out.contains("href=\"../style.css\""), "root_path: {out}"); @@ -461,7 +489,7 @@ fn x() {} 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", @@ -469,7 +497,7 @@ fn x() {} "
", "
", "", - "
",
+        "
",
     ] {
         assert!(
             out.contains(needle),
diff --git a/crates/nuwiki-core/tests/lexer.rs b/crates/nuwiki-core/tests/lexer.rs
index bd0587c..90df673 100644
--- a/crates/nuwiki-core/tests/lexer.rs
+++ b/crates/nuwiki-core/tests/lexer.rs
@@ -179,7 +179,9 @@ fn preformatted_block_parses_language() {
 #[test]
 fn preformatted_block_parses_attrs() {
     let lexed = lex("{{{rust class=\"hl\" key=val\nx\n}}}\n");
-    let PreformattedOpen { language, attrs } = &lexed[0] else {
+    let PreformattedOpen {
+        language, attrs, raw, ..
+    } = &lexed[0] else {
         panic!("expected PreformattedOpen");
     };
     assert_eq!(language.as_deref(), Some("rust"));
@@ -187,6 +189,8 @@ fn preformatted_block_parses_attrs() {
     expected.insert("class".into(), "hl".into());
     expected.insert("key".into(), "val".into());
     assert_eq!(attrs, &expected);
+    // The verbatim fence text is preserved for the HTML renderer.
+    assert_eq!(raw, "rust class=\"hl\" key=val");
 }
 
 // ===== Math block =====
diff --git a/crates/nuwiki-core/tests/tags.rs b/crates/nuwiki-core/tests/tags.rs
index c92c727..83afb16 100644
--- a/crates/nuwiki-core/tests/tags.rs
+++ b/crates/nuwiki-core/tests/tags.rs
@@ -190,8 +190,9 @@ fn file_tags_are_deduped_in_metadata() {
 fn renderer_emits_div_with_tag_spans() {
     let html = render(":todo:done:\n");
     assert!(html.contains("
")); - assert!(html.contains("todo")); - assert!(html.contains("done")); + // Bare tag name as the id (vimwiki parity), so `[[Page#todo]]` resolves. + assert!(html.contains("todo")); + assert!(html.contains("done")); } #[test] diff --git a/crates/nuwiki-lsp/src/export.rs b/crates/nuwiki-lsp/src/export.rs index a8f8049..6f0f1a1 100644 --- a/crates/nuwiki-lsp/src/export.rs +++ b/crates/nuwiki-lsp/src/export.rs @@ -227,17 +227,31 @@ pub fn render_page_html( } let mut r = HtmlRenderer::new(); - if let Some(ext) = wiki_extension.filter(|e| !e.trim_start_matches('.').is_empty()) { + { + // Same-page anchor links (`[[#Section]]`) resolve to the *current* + // page's html file plus the fragment (`index.html#Section`), matching + // vimwiki — rather than a bare `#Section`. + let current_html = format!("{}.html", name.rsplit('/').next().unwrap_or(name)); // Strip the wiki extension from page links before the default resolver // turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather // than `todo.wiki.html`. Only wiki/interwiki targets are touched; // file:/local: paths keep their literal extension. - let ext = ext.to_string(); + let ext = wiki_extension + .filter(|e| !e.trim_start_matches('.').is_empty()) + .map(|e| e.to_string()); r = r.with_link_resolver(move |target| { + if matches!(target.kind, LinkKind::AnchorOnly) { + return match &target.anchor { + Some(a) => format!("{current_html}#{a}"), + None => current_html.clone(), + }; + } let mut t = target.clone(); - if matches!(t.kind, LinkKind::Wiki | LinkKind::Interwiki) { - if let Some(p) = t.path.take() { - t.path = Some(crate::index::strip_wiki_extension(&p, Some(&ext)).to_string()); + if let Some(ext) = &ext { + if matches!(t.kind, LinkKind::Wiki | LinkKind::Interwiki) { + if let Some(p) = t.path.take() { + t.path = Some(crate::index::strip_wiki_extension(&p, Some(ext)).to_string()); + } } } nuwiki_core::render::html::default_link_resolver(&t) @@ -382,14 +396,195 @@ pub fn css_path(cfg: &HtmlConfig) -> PathBuf { cfg.html_path.join(&cfg.css_name) } -/// Minimal default CSS. Used by `nuwiki.export.*` commands when no CSS -/// file exists yet at [`css_path`]. Deliberately tiny — users are -/// expected to replace it with their own. -pub const DEFAULT_CSS: &str = - "body{font-family:sans-serif;max-width:46em;margin:2em auto;padding:0 1em;line-height:1.55}\ -h1,h2,h3,h4,h5,h6{font-family:serif;line-height:1.2}\ -pre{background:#f4f4f4;padding:.5em;overflow:auto}\ -code{background:#f4f4f4;padding:.1em .25em;border-radius:3px}\ -table{border-collapse:collapse}\ -table td,table th{border:1px solid #ccc;padding:.25em .5em}\ -ul.toc{padding-left:1.5em}\n"; +/// Default CSS written by `nuwiki.export.*` commands when no CSS file exists +/// yet at [`css_path`]. This is vimwiki's stock `style.css` verbatim (MIT +/// licensed) so exports look identical to upstream out of the box and the +/// `.header` / `.tag` / `.toc` / `done*` classes our HTML emits are styled. +pub const DEFAULT_CSS: &str = r#"body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;; + margin: 2em 4em 2em 4em; + font-size: 120%; + line-height: 130%; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: bold; + line-height:100%; + margin-top: 1.5em; + margin-bottom: 0.5em; +} + +h1 {font-size: 2em; color: #000000;} +h2 {font-size: 1.8em; color: #404040;} +h3 {font-size: 1.6em; color: #707070;} +h4 {font-size: 1.4em; color: #909090;} +h5 {font-size: 1.2em; color: #989898;} +h6 {font-size: 1em; color: #9c9c9c;} + +p, pre, blockquote, table, ul, ol, dl { + margin-top: 1em; + margin-bottom: 1em; +} + +ul ul, ul ol, ol ol, ol ul { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +li { margin: 0.3em auto; } + +ul { + margin-left: 2em; + padding-left: 0; +} + +dt { font-weight: bold; } + +img { border: none; } + +pre { + border-left: 5px solid #dcdcdc; + background-color: #f5f5f5; + padding-left: 1em; + font-family: Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace; + font-size: 0.8em; + border-radius: 6px; +} + +p > a { + color: white; + text-decoration: none; + font-size: 0.7em; + padding: 3px 6px; + border-radius: 3px; + background-color: #1e90ff; + text-transform: uppercase; + font-weight: bold; +} + +p > a:hover { + color: #dcdcdc; + background-color: #484848; +} + +li > a { + color: #1e90ff; + font-weight: bold; + text-decoration: none; +} + +li > a:hover { color: #ff4500; } + +blockquote { + color: #686868; + font-size: 0.8em; + line-height: 120%; + padding: 0.8em; + border-left: 5px solid #dcdcdc; +} + +th, td { + border: 1px solid #ccc; + padding: 0.3em; +} + +th { background-color: #f0f0f0; } + +hr { + border: none; + border-top: 1px solid #ccc; + width: 100%; +} + +del { + text-decoration: line-through; + color: #777777; +} + +.toc li { list-style-type: none; } + +.todo { + font-weight: bold; + background-color: #ff4500 ; + color: white; + font-size: 0.8em; + padding: 3px 6px; + border-radius: 3px; +} + +.justleft { text-align: left; } +.justright { text-align: right; } +.justcenter { text-align: center; } + +.center { + margin-left: auto; + margin-right: auto; +} + +.tag { + background-color: #eeeeee; + font-family: monospace; + padding: 2px; +} + +.header a { + text-decoration: none; + color: inherit; +} + +/* classes for items of todo lists */ + +.rejected { + /* list-style: none; */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAACXBIWXMAAADFAAAAxQEdzbqoAAAAB3RJTUUH4QgEFhAtuWgv9wAAAPZQTFRFmpqam5iYnJaWnJeXnpSUn5OTopCQpoqKpouLp4iIqIiIrYCAt3V1vW1tv2xsmZmZmpeXnpKS/x4e/x8f/yAg/yIi/yQk/yUl/yYm/ygo/ykp/yws/zAw/zIy/zMz/zQ0/zU1/zY2/zw8/0BA/0ZG/0pK/1FR/1JS/1NT/1RU/1VV/1ZW/1dX/1pa/15e/19f/2Zm/2lp/21t/25u/3R0/3p6/4CA/4GB/4SE/4iI/46O/4+P/52d/6am/6ur/66u/7Oz/7S0/7e3/87O/9fX/9zc/93d/+Dg/+vr/+3t/+/v//Dw//Ly//X1//f3//n5//z8////gzaKowAAAA90Uk5T/Pz8/Pz8/Pz8/Pz8/f39ppQKWQAAAAFiS0dEEnu8bAAAAACuSURBVAhbPY9ZF4FQFEZPSKbIMmWep4gMGTKLkIv6/3/GPbfF97b3w17rA0kQOPgvAeHW6uJ6+5h7HqLdwowgOzejXRXBdx6UdSquml4xuOMBHHNU0clTzeSUA6EhF8V8kqroluMiU6HKcuf4phGPr1o2q9kYZWwNq1qfRRmTaXpqsyjj17KkWCxKBUBgXWueHIyiAIg18gsse4KHkLF5IKIY10WQgv7fOy4ST34BRiopZ8WLNrgAAAAASUVORK5CYII=); + background-repeat: no-repeat; + background-position: 0 .2em; + padding-left: 1.5em; +} +.done0 { + /* list-style: none; */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAA7SURBVCiR7dMxEgAgCANBI3yVRzF5KxNbW6wsuH7LQ2YKQK1mkswBVERYF5Os3UV3gwd/jF2SkXy66gAZkxS6BniubAAAAABJRU5ErkJggg==); + background-repeat: no-repeat; + background-position: 0 .2em; + padding-left: 1.5em; +} +.done1 { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABtSURBVCiR1ZO7DYAwDER9BDmTeZQMFXmUbGYpOjrEryA0wOvO8itOslFrJYAug5BMM4BeSkmjsrv3aVTa8p48Xw1JSkSsWVUFwD05IqS1tmYzk5zzae9jnVVVzGyXb8sALjse+euRkEzu/uirFomVIdDGOLjuAAAAAElFTkSuQmCC); + background-repeat: no-repeat; + background-position: 0 .15em; + padding-left: 1.5em; +} +.done2 { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAB1SURBVCiRzdO5DcAgDAVQGxjAYgTvxlDIu1FTIRYAp8qlFISkSH7l5kk+ZIwxKiI2mIyqWoeILYRgZ7GINDOLjnmF3VqklKCUMgTee2DmM661Qs55iI3Zm/1u5h9sm4ig9z4ERHTFzLyd4G4+nFlVrYg8+qoF/c0kdpeMsmcAAAAASUVORK5CYII=); + background-repeat: no-repeat; + background-position: 0 .15em; + padding-left: 1.5em; +} +.done3 { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABoSURBVCiR7dOxDcAgDATA/0DtUdiKoZC3YhLkHjkVKF3idJHiztKfvrHZWnOSE8Fx95RJzlprimJVnXktvXeY2S0SEZRSAAAbmxnGGKH2I5T+8VfxPhIReQSuuY3XyYWa3T2p6quvOgGrvSFGlewuUAAAAABJRU5ErkJggg==); + background-repeat: no-repeat; + background-position: 0 .15em; + padding-left: 1.5em; +} +.done4 { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAzgAAAM4BlP6ToAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIISURBVDiNnZQ9SFtRFMd/773kpTaGJoQk1im4VDpWQcTNODhkFBcVTCNCF0NWyeDiIIiCm82QoIMIUkHUxcFBg1SEQoZszSat6cdTn1qNue92CMbEr9Of5+vd8/3nPux/3O+f8h6ukUil3sVg0+M+4cFxk42/jH2wAqqqKSCSiPQdwcHHAnDHH9s/tN1h8V28ETdP+eU8fT9Nt62ancYdIPvJNtsu87bmjrJlrTDVM4RROJs1JrHPrD4Bar7A6cpc54iKOaTdJXCUI2UMVrQZ0Js7YPN18ECKkYNQcJe/OE/4dZsw7VqNXQMvHy3QZXQypQ6ycrtwDjf8aJ+PNEDSCzLpn7+m2pD8ZKHlKarYhy6XjEoCYGcN95qansQeA3fNdki+SaJZGTMQIOoL3W/Z89rxv+tokubNajlvk/vm+LFpF2XnUKZHI0I+QrI7Dw0OZTqdzUkpsM7mZTyfy5OPGyw1tK7AFSvmB/Ks8w8YwbUYbe6/3QEKv0vugfxWPnMLJun+d/kI/WLdizpNjMbAIKrhMF4OuwadBALqqs+RfInwUvuNi+fBd+wjogfogAFVRmffO02q01mZZ0HHdgXIzdz0QQLPezIQygX6llxNKKgOFARYCC49CqhoHIUTlss/Vx2phlYwjw8j1CAlfAiwQiJpiy7o1VHnsG5FISkoJu7Q/2YmmaV+i0ei7v38L2CBguSi5AAAAAElFTkSuQmCC); + background-repeat: no-repeat; + background-position: 0 .15em; + padding-left: 1.5em; +} + +code { + font-family: Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; + padding: 0px 3px; + display: inline-block; + color: #52595d; + border: 1px solid #ccc; + background-color: #f9f9f9; +} +"#; diff --git a/crates/nuwiki-lsp/tests/html_export.rs b/crates/nuwiki-lsp/tests/html_export.rs index 5369282..4b4f35e 100644 --- a/crates/nuwiki-lsp/tests/html_export.rs +++ b/crates/nuwiki-lsp/tests/html_export.rs @@ -385,19 +385,27 @@ fn render_page_html_numbers_headers_when_enabled() { ) .unwrap(); assert!( - html.contains("

1. Intro

"), + html.contains( + "" + ), "got: {html}" ); assert!( - html.contains("

1.1. Background

"), + html.contains( + "" + ), "got: {html}" ); assert!( - html.contains("

1.2. Methods

"), + html.contains( + "" + ), "got: {html}" ); assert!( - html.contains("

2. Results

"), + html.contains( + "" + ), "got: {html}" ); } @@ -419,14 +427,45 @@ fn render_page_html_numbering_skips_headers_above_start_level() { ) .unwrap(); assert!( - html.contains("

Title

"), + html.contains( + "" + ), "h1 unnumbered, got: {html}" ); assert!( - html.contains("

1 Section

"), + html.contains( + "" + ), + "got: {html}" + ); + assert!( + html.contains( + "" + ), + "got: {html}" + ); +} + +#[test] +fn render_page_html_same_page_anchor_uses_current_filename() { + // vimwiki parity (#4): `[[#Section]]` resolves to `.html#Section`, + // not a bare `#Section`, where is the file being rendered. + let ast = parse("= Contents =\n\n[[#Contents]]\n"); + let c = cfg("/tmp/x"); + let html = export::render_page_html( + &ast, + None, + "index", + DiaryDate::from_ymd(2026, 5, 11).unwrap(), + &c.html, + None, + HashMap::new(), + ) + .unwrap(); + assert!( + html.contains(""), "got: {html}" ); - assert!(html.contains("

1.1 Sub

"), "got: {html}"); } #[test] @@ -443,8 +482,18 @@ fn render_page_html_no_header_numbering_by_default() { HashMap::new(), ) .unwrap(); - assert!(html.contains("

Intro

"), "got: {html}"); - assert!(html.contains("

Sub

"), "got: {html}"); + assert!( + html.contains( + "
" + ), + "got: {html}" + ); + assert!( + html.contains( + "" + ), + "got: {html}" + ); } #[test]