diff --git a/lib/jsr.json b/lib/jsr.json
index 0eb5e0b26c128ea7281dab8b0b3bbdff2abea360..b7be3b5673a4857b728d4ebca2f56488ced2a87f 100644
--- a/lib/jsr.json
+++ b/lib/jsr.json
@@ -1,6 +1,6 @@
{
"name": "@clo/markodown",
- "version": "1.0.0-rc.5",
+ "version": "1.0.0-rc.6",
"license": "ISC",
"exports": {
".": "./mod.ts",
diff --git a/src/outline.rs b/src/outline.rs
index 4625eee05d7e5c20337e40e5c25346eec0a8cb67..8ac8747b8edae387402e42ad0b5991d13d104d20 100644
--- a/src/outline.rs
+++ b/src/outline.rs
@@ -11,7 +11,7 @@ use std::collections::HashSet;
use crate::marko_ast::AttributeValue;
use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText};
-use crate::typescript::{parse_expr, parse_expr_extra};
+use crate::typescript::parse_expr_extra;
/// A heading entry for the outline, with content reference for hoisting.
#[derive(Debug, Clone)]
diff --git a/src/plugin/tags.rs b/src/plugin/tags.rs
index 6776720b69d3e2fabf45dac4f1834e17ca1dafd7..fdce64a09d4d7ddcda2a432eac126316e7fea080 100644
--- a/src/plugin/tags.rs
+++ b/src/plugin/tags.rs
@@ -372,3 +372,62 @@ impl InlineRule for Rule {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::find_same_line_close;
+ use crate::marko;
+
+ #[test]
+ fn test_find_same_line_close_with_nested_tags() {
+ // Should find > after the nested
+ let content = "aaa mmmm>";
+ let result = find_same_line_close(content, "h3");
+ assert!(result.is_some(), "Should find closing tag");
+ let (before, close) = result.unwrap();
+ assert_eq!(before, "aaa mmmm");
+ assert_eq!(close, ">");
+ }
+
+ #[test]
+ fn test_block_tag_with_inline_content() {
+ // the string
+ // 'bbbbbbb'>aaa mmmm>
+ //
+ // 'string' > id < /regexp/ >
+ let src = "aaa mmmm>";
+ let open = marko::parse_open(src).unwrap();
+ assert_eq!(open.tag_name(), "h3");
+ assert_eq!(open.src, "");
+
+ // The rest after the tag
+ let rest = &src[open.src.len()..];
+ assert_eq!(rest, "aaa mmmm>");
+
+ // find_same_line_close should find the > at the end
+ let result = find_same_line_close(rest, "h3");
+ assert!(result.is_some(), "Should find closing tag for h3");
+ let (content, close) = result.unwrap();
+ assert_eq!(content, "aaa mmmm");
+ assert_eq!(close, ">");
+ }
+
+ #[test]
+ fn test_nested_greater_than() {
+ let src = "aaa mmmm>";
+ let open = marko::parse_open(src).unwrap();
+ assert_eq!(open.tag_name(), "h3");
+ assert_eq!(open.src, "");
+
+ // The rest after the tag
+ let rest = &src[open.src.len()..];
+ assert_eq!(rest, "aaa mmmm>");
+
+ // find_same_line_close should find the > at the end
+ let result = find_same_line_close(rest, "h3");
+ assert!(result.is_some(), "Should find closing tag for h3");
+ let (content, close) = result.unwrap();
+ assert_eq!(content, "aaa mmmm");
+ assert_eq!(close, ">");
+ }
+}
diff --git a/src/plugin/template.rs b/src/plugin/template.rs
index c601749aa54ab2707a97ca5ef82ec5c89b8bfb87..84452f8ea9e61f694773708d8672fc0835395d6f 100644
--- a/src/plugin/template.rs
+++ b/src/plugin/template.rs
@@ -25,7 +25,8 @@ impl InlineRule for Rule {
let allocator = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
- .with_typescript(true);
+ .with_typescript(true)
+ .with_jsx(false);
let expr = match Parser::new(&allocator, expr_source, source_type).parse_expression() {
Ok(expr) => expr,
diff --git a/src/typescript.rs b/src/typescript.rs
index 5fb3d586715645556e77667fda6806b0c18a8ff5..2c32b4b6edf1768ae96d06557aecc72d2c1312bd 100644
--- a/src/typescript.rs
+++ b/src/typescript.rs
@@ -39,7 +39,8 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
let source_type = SourceType::default()
.with_module(true)
- .with_typescript(true);
+ .with_typescript(true)
+ .with_jsx(false);
let mut result = oxc_parser::Parser::new(allocator, source, source_type).parse();
if !result.errors.is_empty() && result.program.body.is_empty() {
@@ -111,7 +112,8 @@ pub fn parse_expr_extra<'alloc, 'src: 'alloc>(
let source_type = SourceType::default()
.with_module(true)
- .with_typescript(true);
+ .with_typescript(true)
+ .with_jsx(false);
let expr = match oxc_parser::Parser::new(allocator, source, source_type).parse_expression() {
Ok(expr) => expr,
@@ -185,25 +187,30 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result {
let rest = l.peek_rest();
let mut allocator = Allocator::new();
- // First, try parsing the full source and using AST walking to strip trailing `>`.
+ // First, try parsing the full source and using AST walking to find the earliest `>`.
// This handles balanced cases like `arr[a > 0]` and `fn(a > b)` correctly.
+ // We walk down the left spine of binary expressions, looking for any `>` operator.
+ // When we find one, we record the span of its left operand. We keep walking to find
+ // the innermost `>` (which appears earliest in the source).
if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) {
let mut e = &expr;
+ let mut result_end = expr.span().end; // default: whole expression
+
while let Expression::BinaryExpression(bin) = e {
if bin.operator == BinaryOperator::GreaterThan
|| bin.operator == BinaryOperator::GreaterEqualThan
|| bin.operator == BinaryOperator::ShiftRight
|| bin.operator == BinaryOperator::ShiftRightZeroFill
{
- e = &bin.left;
- } else {
- break;
+ // Found a `>`, update result to be left side's span
+ result_end = bin.left.span().end;
}
+ // Continue walking left to find any nested `>` operators
+ e = &bin.left;
}
- let length = e.span().end;
- l.advance(length);
- return Ok(length);
+ l.advance(result_end);
+ return Ok(result_end);
}
// If full parse failed (e.g., due to JSX-like content after the expression),
@@ -871,4 +878,53 @@ mod tests {
let result = scan_first_statement_forbid_trailing(source);
assert!(result.is_ok());
}
+
+ #[test]
+ fn test_no_gt_with_marko_tags_after() {
+ assert_eq!(parse_expr_no_gt("'test'>"), Ok(6));
+ assert_eq!(parse_expr_no_gt("'a'>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>text>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>b>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>b c>"), Ok(3));
+ }
+
+ #[test]
+ fn test_no_gt_double_quoted_string() {
+ assert_eq!(parse_expr_no_gt("\"hello\">>>"), Ok(7));
+ }
+
+ #[test]
+ fn test_no_gt_string_followed_by_gt() {
+ // Simple string then >
+ assert_eq!(parse_expr_no_gt("'test'>"), Ok(6));
+ assert_eq!(parse_expr_no_gt("\"test\">"), Ok(6));
+ }
+
+ #[test]
+ fn test_no_gt_string_then_jsx_like() {
+ assert_eq!(parse_expr_no_gt("'a'>"), Ok(3)); // just 'a'
+ assert_eq!(parse_expr_no_gt("'a'>b"), Ok(3)); // just 'a'
+ }
+
+ #[test]
+ fn test_no_gt_string_then_jsx_variations() {
+ assert_eq!(parse_expr_no_gt("'x'>c"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>c>"), Ok(3));
+ assert_eq!(parse_expr_no_gt("'x'>aaa"), Ok(3));
+ }
+
+ #[test]
+ fn test_no_gt_other_ops() {
+ assert_eq!(parse_expr_no_gt("3 + 4 ___"), Ok(5));
+ assert_eq!(parse_expr_no_gt("3 + 4 > 4 ___"), Ok(5));
+ assert_eq!(parse_expr_no_gt("3 + 4 < 4 ___"), Ok(9));
+ assert_eq!(parse_expr_no_gt("3 < 4 ___"), Ok(5));
+ }
}