From e8d9aa0ccc17ba1add4757022b7121d0f731858c Mon Sep 17 00:00:00 2001 From: clover caruso Date: Tue, 17 Feb 2026 00:42:05 -0800 Subject: [PATCH] fix: some stuff --- src/component_transforms.rs | 15 +++++++- src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++- src/plugin/tags.rs | 40 ++++++++++++++++++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/component_transforms.rs b/src/component_transforms.rs index 0397b6f2e5d0f7fa79d7949d52a31daa07b32c90..27c4e4727061ce90359d8cce8555fa1853136696 100644 --- a/src/component_transforms.rs +++ b/src/component_transforms.rs @@ -6,7 +6,9 @@ use markdown_it::{Node, NodeValue, Renderer}; use crate::marko_ast::OpenOwned; -use crate::plugin::tags::{MarkoBlockComplete, MarkoClose, MarkoOpen, MarkoOpenWithText}; +use crate::plugin::tags::{ + MarkoBlockComplete, MarkoClose, MarkoCloseWithText, MarkoOpen, MarkoOpenWithText, +}; use crate::ComponentImports; /// Component name suffix to avoid collisions @@ -446,6 +448,17 @@ fn transform_heading(node: &mut Node) { } } } + + if let Some(marko_close) = node.cast_mut::() { + if let Some(tag_name) = &marko_close.tag_name { + if parse_heading_level(tag_name).is_some() { + // Replace with generic close tag + marko_close.content = "".to_string(); + marko_close.tag_name = None; + marko_close.tag_name_len = None; + } + } + } } /// Parse h1-h6 tag names and return the level diff --git a/src/lib.rs b/src/lib.rs index 10f8d9efd583d61305bcaa23c13cee1c0a73c784..1a28f0bf9cdf8612993047fbe09c4e72c5c83d86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod wasm; use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; use oxc_span::Span; -use plugin::tags::{MarkoClose, MarkoOpen}; +use plugin::tags::{MarkoClose, MarkoCloseWithText, MarkoOpen}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use wasm_bindgen::prelude::wasm_bindgen; @@ -312,6 +312,7 @@ fn has_marko_features(node: &markdown_it::Node) -> bool { || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() + || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() @@ -395,6 +396,48 @@ fn validate_marko_tags( ); } } + } else if let Some(close) = node.cast::() { + let (close_start, _) = node.srcmap.unwrap().get_byte_offsets(); + let close_start = close_start as u32 + offset; + // Close tag name starts after ' { + // valid + } + (None, Some(_)) => { + // closes anything - valid + } + (Some(name), Some((top_name, top_span))) => { + errors.push( + OxcDiagnostic::error(format!( + "Mismatched closing tag: expected , found " + )) + .with_labels(vec![ + top_span.label("opened here"), + close_span.unwrap().label("closed here"), + ]), + ); + } + (Some(name), None) => { + errors.push( + OxcDiagnostic::error(format!( + "Closing tag without matching open" + )) + .with_label(close_span.unwrap()), + ); + } + (None, None) => { + errors.push( + OxcDiagnostic::error("Closing tag without matching open") + .with_label(Span::new(close_start, close_start + 3)), + ); + } + } } // Recurse into children @@ -1112,4 +1155,36 @@ mod tests { "explicit link import should be present" ); } + + #[test] + fn close_tag_with_same_line_text() { + let out = run("
\n
trailing text\n"); + assert!(out.contains(""), "close tag missing"); + assert!( + out.contains("trailing text"), + "same-line text after close tag missing" + ); + } + + #[test] + fn close_tag_with_same_line_text_validated() { + let result = transform( + "
\ncontent\n
after", + None, + None, + None, + None, + false, + None, + ); + assert!( + result.is_ok(), + "should not error on close tag with trailing text" + ); + let out = result.unwrap().text; + assert!( + out.contains("after"), + "trailing text should appear in output" + ); + } } diff --git a/src/plugin/tags.rs b/src/plugin/tags.rs index fdce64a09d4d7ddcda2a432eac126316e7fea080..6e6574d53775202c38ac15bf9f314e366d7f5445 100644 --- a/src/plugin/tags.rs +++ b/src/plugin/tags.rs @@ -35,6 +35,25 @@ impl NodeValue for MarkoClose { } } +/// A closing Marko tag with same-line text: text here +/// The text is parsed as inline markdown (children) but not wrapped in

+#[derive(Debug)] +pub struct MarkoCloseWithText { + pub content: String, + pub tag_name: Option, // None for + /// Length of the tag name (for error span highlighting), None for + pub tag_name_len: Option, +} + +impl NodeValue for MarkoCloseWithText { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.text_raw(&self.content); + fmt.text_raw("\n"); + fmt.contents(&node.children); + fmt.text_raw("\n"); + } +} + /// A self-closing tag: , #[derive(Debug)] pub struct MarkoSelfClosing { @@ -309,8 +328,25 @@ impl BlockRule for Rule { let rest_after_tag = &unbounded_src[close.length as usize..]; let rest_of_line = rest_after_tag.lines().next().unwrap_or(""); if !rest_of_line.trim().is_empty() { - // TODO: content after close tag on same line - todo!("content after close tag: {}", rest_of_line.trim()); + let text = rest_of_line.trim(); + + // Calculate byte offset of the text for source mapping + let text_start_in_line = close.length as usize + + (rest_of_line.len() - rest_of_line.trim_start().len()); + let line_start = state.line_offsets[state.line].first_nonspace; + let text_start = line_start + text_start_in_line; + + let mapping = vec![(0, text_start)]; + + let mut node = Node::new(MarkoCloseWithText { + content: close_text.to_string(), + tag_name: close.tag_name.map(|s| s.to_string()), + tag_name_len: close.tag_name.map(|s| s.len()), + }); + node.children + .push(Node::new(InlineRoot::new(text.to_string(), mapping))); + + return Some((node, lines_consumed as usize)); } Some(( -- 2.54.0