diff --git a/crates/nuwiki-core/src/syntax/vimwiki/parser.rs b/crates/nuwiki-core/src/syntax/vimwiki/parser.rs index a38b0da..e9d2176 100644 --- a/crates/nuwiki-core/src/syntax/vimwiki/parser.rs +++ b/crates/nuwiki-core/src/syntax/vimwiki/parser.rs @@ -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) { + 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 { diff --git a/crates/nuwiki-core/tests/transclusion_attrs.rs b/crates/nuwiki-core/tests/transclusion_attrs.rs new file mode 100644 index 0000000..4fb51de --- /dev/null +++ b/crates/nuwiki-core/tests/transclusion_attrs.rs @@ -0,0 +1,60 @@ +//! Cluster 8 — transclusion attribute parsing. The basic +//! `{{url|alt|key=val}}` shape was already wired; this test pins +//! down quote-stripping on quoted values (`key="quoted value"`) +//! so an HTML renderer doesn't double-emit `"` artefacts. + +use nuwiki_core::ast::{BlockNode, InlineNode}; +use nuwiki_core::render::{HtmlRenderer, Renderer}; +use nuwiki_core::syntax::vimwiki::VimwikiSyntax; +use nuwiki_core::syntax::SyntaxPlugin; + +fn parse(src: &str) -> nuwiki_core::ast::DocumentNode { + VimwikiSyntax::new().parse(src) +} + +fn first_transclusion(doc: &nuwiki_core::ast::DocumentNode) -> &nuwiki_core::ast::TransclusionNode { + let para = match &doc.children[0] { + BlockNode::Paragraph(p) => p, + _ => panic!("expected paragraph"), + }; + for child in ¶.children { + if let InlineNode::Transclusion(t) = child { + return t; + } + } + panic!("no transclusion in paragraph"); +} + +#[test] +fn transclusion_attrs_strip_double_quotes() { + let doc = parse(r#"{{cat.png|cat|style="border: 1px"}}"#); + let t = first_transclusion(&doc); + assert_eq!(t.url, "cat.png"); + assert_eq!(t.alt.as_deref(), Some("cat")); + assert_eq!( + t.attrs.get("style").map(String::as_str), + Some("border: 1px") + ); +} + +#[test] +fn transclusion_attrs_strip_single_quotes() { + let doc = parse("{{cat.png|cat|class='thumb'}}"); + let t = first_transclusion(&doc); + assert_eq!(t.attrs.get("class").map(String::as_str), Some("thumb")); +} + +#[test] +fn transclusion_attrs_leave_unquoted_values_alone() { + let doc = parse("{{cat.png|cat|width=200}}"); + let t = first_transclusion(&doc); + assert_eq!(t.attrs.get("width").map(String::as_str), Some("200")); +} + +#[test] +fn transclusion_renders_clean_html_with_quoted_attr() { + let doc = parse(r#"{{cat.png|cat|style="border: 1px"}}"#); + let out = HtmlRenderer::new().render_to_string(&doc).unwrap(); + assert!(out.contains(r#"style="border: 1px""#), "got: {out}"); + assert!(!out.contains(""")); +}