phase 17: html export commands + extended template vars
HtmlRenderer (nuwiki-core):
- `with_var(k, v)` / `with_vars(map)` add arbitrary `{{key}}`
substitution alongside the existing `{{title}}` / `{{content}}`.
Substitution order: content → title → user vars, so a rendered body
that happens to contain a literal `{{title}}` isn't double-processed.
WikiConfig (nuwiki-lsp/config):
- New `html: HtmlConfig` field with per-wiki export options matching
vimwiki's `g:vimwiki_*` keys: `html_path`, `template_path`,
`template_default`, `template_ext`, `template_date_format`,
`css_name`, `auto_export`, `html_filename_parameterization`,
`exclude_files`. Defaults: `<root>/_html` and `<root>/_templates`.
- `RawWiki` accepts all of the above through `initializationOptions`
(and `workspace/didChangeConfiguration` reload).
New `export` module (pure helpers):
- `output_path_for` — page name → on-disk HTML path. Slugifies only
the final segment when `html_filename_parameterization = true` so
the `diary/` subdir survives.
- `template_for` — primary/fallback path lookup tuple.
- `fallback_template` — minimal in-memory page used when neither
template file exists.
- `format_date` — strftime subset (`%Y`/`%m`/`%d`/`%%`) so we don't
depend on `chrono`/`time`.
- `build_toc_html` — nested `<ul class="toc">` from the doc's
headings, with HTML escaping.
- `compute_root_path` — `../` prefix per directory depth for
stylesheet hrefs in nested pages.
- `is_excluded` — glob matcher supporting `*`, `**`, `?`. Used by
the `allToHtml*` walker against `exclude_files`.
- `render_page_html` — wires the above into HtmlRenderer with
`{{date}}`, `{{root_path}}`, `{{toc}}`, `{{css}}` populated.
- `DEFAULT_CSS` — minimal stylesheet bundled for first-time exports.
New commands:
- `nuwiki.export.currentToHtml` — render the current buffer; honours
`%nohtml` and `%template`; ensures CSS exists.
- `nuwiki.export.allToHtml` — walk the wiki root, skip pages matched
by `exclude_files` and `%nohtml`, and only re-export pages whose
source is newer than the existing HTML (incremental behaviour
matching `:VimwikiAll2HTML`).
- `nuwiki.export.allToHtmlForce` — same but unconditional re-export.
- `nuwiki.export.browse` — alias of currentToHtml that returns the
`file://` URL of the rendered page in the response so the client
can open it.
- `nuwiki.export.rss` — write `<html_path>/rss.xml` listing the
wiki's diary entries newest-first.
- `did_save` handler — runs the auto-export equivalent when the
resolved wiki has `auto_export = true`.
LSP capability:
- `text_document_sync` switched from `Kind(FULL)` to `Options(...)`
so the server can register a `save` notification (otherwise
clients never push `did_save` events).
Tests: 33 new in `phase17_html_export.rs` covering HtmlConfig
defaults + JSON-shape config, the `format_date` subset (including
the `%%` escape and pass-through for unsupported specifiers),
root-path computation, output-path slugification rules, the glob
matcher (single-star, globstar, question-mark, non-match),
template_for primary/fallback selection, render_page_html for the
title/content/date/root_path triad and the override-by-extras flow,
and side-effecting disk paths (write_page creates parents, picks up
on-disk template, RSS is newest-first, ensure_css is idempotent).
Total 357 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
//!
|
||||
//! Walks a `DocumentNode` and writes HTML5 fragments. The renderer is
|
||||
//! configured with a link resolver (callback that turns a `LinkTarget` into
|
||||
//! a URL string) and, optionally, an enclosing template with `{{title}}` /
|
||||
//! `{{content}}` placeholders (SPEC.md §6.8).
|
||||
//! a URL string) and, optionally, an enclosing template with substitution
|
||||
//! placeholders (SPEC.md §6.8 + §12.8).
|
||||
//!
|
||||
//! Without a template the output is just the body markup, so callers are
|
||||
//! free to embed it in their own page shell. With a template the rendered
|
||||
//! body replaces `{{content}}` and the document's `metadata.title`
|
||||
//! replaces `{{title}}`.
|
||||
//! Substitution model: `{{content}}` and `{{title}}` are computed from
|
||||
//! the rendered body and the document's metadata; `with_var(k, v)` /
|
||||
//! `with_vars(map)` add arbitrary key→value pairs (Phase 17 ships
|
||||
//! `{{date}}`, `{{root_path}}`, `{{toc}}`). Order: `{{content}}` is
|
||||
//! substituted first so a body that happens to contain a literal
|
||||
//! `{{title}}` isn't itself rewritten in the next pass.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -32,6 +35,7 @@ pub type LinkResolver = Arc<dyn Fn(&LinkTarget) -> String + Send + Sync>;
|
||||
pub struct HtmlRenderer {
|
||||
link_resolver: LinkResolver,
|
||||
template: Option<String>,
|
||||
vars: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for HtmlRenderer {
|
||||
@@ -45,6 +49,7 @@ impl HtmlRenderer {
|
||||
Self {
|
||||
link_resolver: Arc::new(default_link_resolver),
|
||||
template: None,
|
||||
vars: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +64,25 @@ impl HtmlRenderer {
|
||||
}
|
||||
|
||||
/// Wrap the rendered body in a template. `{{title}}` and `{{content}}`
|
||||
/// are substituted; everything else passes through verbatim.
|
||||
/// are substituted automatically; additional `{{key}}` placeholders
|
||||
/// resolve via `with_var` / `with_vars`. Anything unresolved passes
|
||||
/// through verbatim.
|
||||
pub fn with_template(mut self, template: impl Into<String>) -> Self {
|
||||
self.template = Some(template.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a single template variable. Re-use to overwrite.
|
||||
pub fn with_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.vars.insert(key.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace the entire template variable map.
|
||||
pub fn with_vars(mut self, vars: HashMap<String, String>) -> Self {
|
||||
self.vars = vars;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlRenderer {
|
||||
@@ -77,9 +96,13 @@ impl Renderer for HtmlRenderer {
|
||||
// Replace `{{content}}` first so its substituted body, which may
|
||||
// legitimately contain the literal `{{title}}`, isn't itself
|
||||
// rewritten in the next pass.
|
||||
let with_content = template.replace("{{content}}", body_str);
|
||||
let final_doc = with_content.replace("{{title}}", title);
|
||||
w.write_all(final_doc.as_bytes())?;
|
||||
let mut out = template.replace("{{content}}", body_str);
|
||||
out = out.replace("{{title}}", title);
|
||||
for (k, v) in &self.vars {
|
||||
let needle = format!("{{{{{k}}}}}");
|
||||
out = out.replace(&needle, v);
|
||||
}
|
||||
w.write_all(out.as_bytes())?;
|
||||
} else {
|
||||
self.render_body(doc, w)?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user