parity(8a): transclusion attr quote-stripping

`{{url|alt|key="quoted value"}}` syntax was already parsed end-to-end
through to `TransclusionNode::attrs`, but the value retained the
surrounding `"…"` (or `'…'`) characters verbatim. The HTML renderer
then escaped them into `"`, producing `style=""border: 1px""`
instead of clean `style="border: 1px"`.

Extracted a small `insert_attr` helper that splits on `=`, trims, and
strips matching single- or double-quote pairs from the value before
inserting. Unquoted values pass through unchanged.

Tests: 4 new in crates/nuwiki-core/tests/transclusion_attrs.rs
covering double-quote stripping, single-quote stripping, unquoted
pass-through, and the clean-HTML rendering shape (no `"`).

Note: this commit covers the "transclusion attrs" half of the
original Cluster 8 audit. The "multi-line list-item continuation
paragraphs" half requires lexer + parser changes (continuation lines
indented past the marker should attach to the prior list item
instead of becoming a sibling paragraph) and is deferred.

Gates: 429 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:51:54 +00:00
parent 214d54e3b9
commit bcef714805
2 changed files with 86 additions and 15 deletions
+26 -15
View File
@@ -1183,27 +1183,14 @@ fn parse_transclusion(toks: &[VimwikiToken], open_idx: usize) -> (InlineNode, us
for window in seps.windows(2) {
let s = window[0];
let e = window[1];
// attr segment is at [s+1..e]; format key=val
let segment = collect_text(&toks[s + 1..e]);
if let Some(eq) = segment.find('=') {
let k = segment[..eq].trim().to_owned();
let v = segment[eq + 1..].trim().to_owned();
if !k.is_empty() {
attrs.insert(k, v);
}
}
insert_attr(&segment, &mut attrs);
}
// Last attribute segment between final sep and close.
if let Some(&last) = seps.last() {
if last + 1 < close_idx && seps.len() >= 2 {
let segment = collect_text(&toks[last + 1..close_idx]);
if let Some(eq) = segment.find('=') {
let k = segment[..eq].trim().to_owned();
let v = segment[eq + 1..].trim().to_owned();
if !k.is_empty() {
attrs.insert(k, v);
}
}
insert_attr(&segment, &mut attrs);
}
}
}
@@ -1219,6 +1206,30 @@ fn parse_transclusion(toks: &[VimwikiToken], open_idx: usize) -> (InlineNode, us
)
}
/// Split a `key=value` attribute segment, strip surrounding quotes from
/// the value, and insert into `attrs`. Empty keys are skipped.
fn insert_attr(segment: &str, attrs: &mut std::collections::HashMap<String, String>) {
let Some(eq) = segment.find('=') else { return };
let k = segment[..eq].trim().to_owned();
if k.is_empty() {
return;
}
let raw = segment[eq + 1..].trim();
let v = strip_quotes(raw).to_owned();
attrs.insert(k, v);
}
fn strip_quotes(s: &str) -> &str {
let b = s.as_bytes();
if b.len() >= 2
&& ((b[0] == b'"' && b[b.len() - 1] == b'"') || (b[0] == b'\'' && b[b.len() - 1] == b'\''))
{
&s[1..s.len() - 1]
} else {
s
}
}
fn collect_text(toks: &[VimwikiToken]) -> String {
let mut out = String::new();
for t in toks {