authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-17 00:42:05-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-18 02:46:06-08:00
loge8d9aa0ccc17ba1add4757022b7121d0f731858c
tree5fed1d6c5db75a2ce6b0bfebee48e29412a5f4e0
parentd25d757a902f98e69e9178395f7857907d37d030
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

fix: some stuff


3 files changed, 128 insertions(+), 4 deletions(-)

src/component_transforms.rs+14-1
......@@ -6,7 +6,9 @@
66use markdown_it::{Node, NodeValue, Renderer};
77
88use crate::marko_ast::OpenOwned;
9use crate::plugin::tags::{MarkoBlockComplete, MarkoClose, MarkoOpen, MarkoOpenWithText};
9use crate::plugin::tags::{
10 MarkoBlockComplete, MarkoClose, MarkoCloseWithText, MarkoOpen, MarkoOpenWithText,
11};
1012use crate::ComponentImports;
1113
1214/// Component name suffix to avoid collisions
......@@ -446,6 +448,17 @@ fn transform_heading(node: &mut Node) {
446448 }
447449 }
448450 }
451
452 if let Some(marko_close) = node.cast_mut::<MarkoCloseWithText>() {
453 if let Some(tag_name) = &marko_close.tag_name {
454 if parse_heading_level(tag_name).is_some() {
455 // Replace with generic close tag
456 marko_close.content = "</>".to_string();
457 marko_close.tag_name = None;
458 marko_close.tag_name_len = None;
459 }
460 }
461 }
449462}
450463
451464/// Parse h1-h6 tag names and return the level
src/lib.rs+76-1
......@@ -8,7 +8,7 @@ pub mod wasm;
88
99use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
1010use oxc_span::Span;
11use plugin::tags::{MarkoClose, MarkoOpen};
11use plugin::tags::{MarkoClose, MarkoCloseWithText, MarkoOpen};
1212use serde::{Deserialize, Serialize};
1313use std::borrow::Cow;
1414use wasm_bindgen::prelude::wasm_bindgen;
......@@ -312,6 +312,7 @@ fn has_marko_features(node: &markdown_it::Node) -> bool {
312312 || node.cast::<plugin::StatementBlock>().is_some()
313313 || node.cast::<MarkoOpen>().is_some()
314314 || node.cast::<MarkoClose>().is_some()
315 || node.cast::<MarkoCloseWithText>().is_some()
315316 || node.cast::<plugin::tags::MarkoSelfClosing>().is_some()
316317 || node.cast::<plugin::tags::MarkoOpenWithText>().is_some()
317318 || node.cast::<plugin::tags::MarkoInlineTag>().is_some()
......@@ -395,6 +396,48 @@ fn validate_marko_tags(
395396 );
396397 }
397398 }
399 } else if let Some(close) = node.cast::<MarkoCloseWithText>() {
400 let (close_start, _) = node.srcmap.unwrap().get_byte_offsets();
401 let close_start = close_start as u32 + offset;
402 // Close tag name starts after '</' (offset +2)
403 let close_span = close
404 .tag_name
405 .as_ref()
406 .map(|name| Span::new(close_start + 2, close_start + 2 + name.len() as u32));
407
408 match (close.tag_name.as_ref(), stack.pop()) {
409 (Some(name), Some((top_name, _))) if name == &top_name => {
410 // valid
411 }
412 (None, Some(_)) => {
413 // </> closes anything - valid
414 }
415 (Some(name), Some((top_name, top_span))) => {
416 errors.push(
417 OxcDiagnostic::error(format!(
418 "Mismatched closing tag: expected </{top_name}>, found </{name}>"
419 ))
420 .with_labels(vec![
421 top_span.label("opened here"),
422 close_span.unwrap().label("closed here"),
423 ]),
424 );
425 }
426 (Some(name), None) => {
427 errors.push(
428 OxcDiagnostic::error(format!(
429 "Closing tag </{name}> without matching open"
430 ))
431 .with_label(close_span.unwrap()),
432 );
433 }
434 (None, None) => {
435 errors.push(
436 OxcDiagnostic::error("Closing tag </> without matching open")
437 .with_label(Span::new(close_start, close_start + 3)),
438 );
439 }
440 }
398441 }
399442
400443 // Recurse into children
......@@ -1112,4 +1155,36 @@ mod tests {
11121155 "explicit link import should be present"
11131156 );
11141157 }
1158
1159 #[test]
1160 fn close_tag_with_same_line_text() {
1161 let out = run("<div>\n</div> trailing text\n");
1162 assert!(out.contains("</div>"), "close tag missing");
1163 assert!(
1164 out.contains("trailing text"),
1165 "same-line text after close tag missing"
1166 );
1167 }
1168
1169 #[test]
1170 fn close_tag_with_same_line_text_validated() {
1171 let result = transform(
1172 "<section>\ncontent\n</section> after",
1173 None,
1174 None,
1175 None,
1176 None,
1177 false,
1178 None,
1179 );
1180 assert!(
1181 result.is_ok(),
1182 "should not error on close tag with trailing text"
1183 );
1184 let out = result.unwrap().text;
1185 assert!(
1186 out.contains("after"),
1187 "trailing text should appear in output"
1188 );
1189 }
11151190}
src/plugin/tags.rs+38-2
......@@ -35,6 +35,25 @@ impl NodeValue for MarkoClose {
3535 }
3636}
3737
38/// A closing Marko tag with same-line text: </div>text here
39/// The text is parsed as inline markdown (children) but not wrapped in <p>
40#[derive(Debug)]
41pub struct MarkoCloseWithText {
42 pub content: String,
43 pub tag_name: Option<String>, // None for </>
44 /// Length of the tag name (for error span highlighting), None for </>
45 pub tag_name_len: Option<usize>,
46}
47
48impl NodeValue for MarkoCloseWithText {
49 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
50 fmt.text_raw(&self.content);
51 fmt.text_raw("\n");
52 fmt.contents(&node.children);
53 fmt.text_raw("\n");
54 }
55}
56
3857/// A self-closing tag: <input />, <Chart data=x />
3958#[derive(Debug)]
4059pub struct MarkoSelfClosing {
......@@ -309,8 +328,25 @@ impl BlockRule for Rule {
309328 let rest_after_tag = &unbounded_src[close.length as usize..];
310329 let rest_of_line = rest_after_tag.lines().next().unwrap_or("");
311330 if !rest_of_line.trim().is_empty() {
312 // TODO: content after close tag on same line
313 todo!("content after close tag: {}", rest_of_line.trim());
331 let text = rest_of_line.trim();
332
333 // Calculate byte offset of the text for source mapping
334 let text_start_in_line = close.length as usize
335 + (rest_of_line.len() - rest_of_line.trim_start().len());
336 let line_start = state.line_offsets[state.line].first_nonspace;
337 let text_start = line_start + text_start_in_line;
338
339 let mapping = vec![(0, text_start)];
340
341 let mut node = Node::new(MarkoCloseWithText {
342 content: close_text.to_string(),
343 tag_name: close.tag_name.map(|s| s.to_string()),
344 tag_name_len: close.tag_name.map(|s| s.len()),
345 });
346 node.children
347 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));
348
349 return Some((node, lines_consumed as usize));
314350 }
315351
316352 Some((