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:
@@ -34,6 +34,64 @@ pub struct WikiConfig {
|
||||
/// Matches vimwiki's `g:vimwiki_diary_index` (default `"diary"`).
|
||||
/// The full path is `<root>/<diary_rel_path>/<diary_index>.wiki`.
|
||||
pub diary_index: String,
|
||||
/// Phase 17 HTML export options. See SPEC §12.8.
|
||||
pub html: HtmlConfig,
|
||||
}
|
||||
|
||||
/// Per-wiki HTML export settings. All paths are absolute (relative
|
||||
/// paths from `initializationOptions` are resolved against the wiki
|
||||
/// root at config-load time).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HtmlConfig {
|
||||
/// Output directory for `nuwiki.export.*` commands.
|
||||
/// Default: `<root>/_html`.
|
||||
pub html_path: PathBuf,
|
||||
/// Template search root. Default: `<root>/_templates`.
|
||||
pub template_path: PathBuf,
|
||||
/// Template name used when a page has no `%template` directive.
|
||||
pub template_default: String,
|
||||
/// Template file extension appended when looking up a template.
|
||||
pub template_ext: String,
|
||||
/// `%Y`/`%m`/`%d` format string passed to `{{date}}`.
|
||||
pub template_date_format: String,
|
||||
/// CSS file name relative to `html_path` (copied/created if missing).
|
||||
pub css_name: String,
|
||||
/// Run `currentToHtml` automatically on `textDocument/didSave`.
|
||||
pub auto_export: bool,
|
||||
/// When true, the output filename for a page is URL-safe-slugified.
|
||||
pub html_filename_parameterization: bool,
|
||||
/// Glob patterns (matched against the page name relative to root)
|
||||
/// to skip during `allToHtml*`.
|
||||
pub exclude_files: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
html_path: PathBuf::new(),
|
||||
template_path: PathBuf::new(),
|
||||
template_default: "default".into(),
|
||||
template_ext: ".tpl".into(),
|
||||
template_date_format: "%Y-%m-%d".into(),
|
||||
css_name: "style.css".into(),
|
||||
auto_export: false,
|
||||
html_filename_parameterization: false,
|
||||
exclude_files: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
/// Synthesise default paths under `root` when the user hasn't
|
||||
/// specified explicit ones. `html_path` defaults to `<root>/_html`
|
||||
/// and `template_path` to `<root>/_templates` per SPEC §12.8.
|
||||
pub fn from_root(root: &Path) -> Self {
|
||||
Self {
|
||||
html_path: root.join("_html"),
|
||||
template_path: root.join("_templates"),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -62,10 +120,12 @@ impl WikiConfig {
|
||||
syntax: "vimwiki".into(),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
html: HtmlConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_root(root: PathBuf) -> Self {
|
||||
let html = HtmlConfig::from_root(&root);
|
||||
Self {
|
||||
name: root
|
||||
.file_name()
|
||||
@@ -76,6 +136,7 @@ impl WikiConfig {
|
||||
syntax: "vimwiki".into(),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
html,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +188,7 @@ impl Config {
|
||||
let wikis = if let Some(list) = raw.wikis {
|
||||
list.into_iter().map(WikiConfig::from).collect()
|
||||
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
|
||||
let html = HtmlConfig::from_root(&root);
|
||||
vec![WikiConfig {
|
||||
name: root
|
||||
.file_name()
|
||||
@@ -137,6 +199,7 @@ impl Config {
|
||||
syntax: raw.syntax.unwrap_or_else(|| "vimwiki".into()),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
html,
|
||||
}]
|
||||
} else if let Some(folder) = first_workspace_folder(params) {
|
||||
vec![WikiConfig::from_root(folder)]
|
||||
@@ -199,6 +262,25 @@ struct RawWiki {
|
||||
diary_rel_path: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_index: Option<String>,
|
||||
// Phase 17 HTML keys — all optional, fall back to per-root defaults.
|
||||
#[serde(default)]
|
||||
html_path: Option<String>,
|
||||
#[serde(default)]
|
||||
template_path: Option<String>,
|
||||
#[serde(default)]
|
||||
template_default: Option<String>,
|
||||
#[serde(default)]
|
||||
template_ext: Option<String>,
|
||||
#[serde(default)]
|
||||
template_date_format: Option<String>,
|
||||
#[serde(default)]
|
||||
css_name: Option<String>,
|
||||
#[serde(default)]
|
||||
auto_export: Option<bool>,
|
||||
#[serde(default)]
|
||||
html_filename_parameterization: Option<bool>,
|
||||
#[serde(default)]
|
||||
exclude_files: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<RawWiki> for WikiConfig {
|
||||
@@ -208,6 +290,34 @@ impl From<RawWiki> for WikiConfig {
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let mut html = HtmlConfig::from_root(&root);
|
||||
if let Some(p) = r.html_path.as_deref().and_then(expand_path) {
|
||||
html.html_path = p;
|
||||
}
|
||||
if let Some(p) = r.template_path.as_deref().and_then(expand_path) {
|
||||
html.template_path = p;
|
||||
}
|
||||
if let Some(s) = r.template_default {
|
||||
html.template_default = s;
|
||||
}
|
||||
if let Some(s) = r.template_ext {
|
||||
html.template_ext = s;
|
||||
}
|
||||
if let Some(s) = r.template_date_format {
|
||||
html.template_date_format = s;
|
||||
}
|
||||
if let Some(s) = r.css_name {
|
||||
html.css_name = s;
|
||||
}
|
||||
if let Some(b) = r.auto_export {
|
||||
html.auto_export = b;
|
||||
}
|
||||
if let Some(b) = r.html_filename_parameterization {
|
||||
html.html_filename_parameterization = b;
|
||||
}
|
||||
if let Some(v) = r.exclude_files {
|
||||
html.exclude_files = v;
|
||||
}
|
||||
WikiConfig {
|
||||
name: r.name.unwrap_or(derived_name),
|
||||
root,
|
||||
@@ -215,6 +325,7 @@ impl From<RawWiki> for WikiConfig {
|
||||
syntax: r.syntax.unwrap_or_else(|| "vimwiki".into()),
|
||||
diary_rel_path: r.diary_rel_path.unwrap_or_else(default_diary_rel_path),
|
||||
diary_index: r.diary_index.unwrap_or_else(default_diary_index),
|
||||
html,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user