authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-16 20:48:18-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-18 02:46:06-08:00
log42a5c17e506a403cb512b3b0911376820175109a
treec78b5fee43f1b729611a1d648c63d834135c53ad
parent7cbb22b69a03badf206949c1ef9d9c399ab97029
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

bugfixing


5 files changed, 128 insertions(+), 12 deletions(-)

lib/jsr.json+1-1
......@@ -1,6 +1,6 @@
11{
22 "name": "@clo/markodown",
3 "version": "1.0.0-rc.5",
3 "version": "1.0.0-rc.6",
44 "license": "ISC",
55 "exports": {
66 ".": "./mod.ts",
src/outline.rs+1-1
......@@ -11,7 +11,7 @@ use std::collections::HashSet;
1111
1212use crate::marko_ast::AttributeValue;
1313use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText};
14use crate::typescript::{parse_expr, parse_expr_extra};
14use crate::typescript::parse_expr_extra;
1515
1616/// A heading entry for the outline, with content reference for hoisting.
1717#[derive(Debug, Clone)]
src/plugin/tags.rs+59
......@@ -372,3 +372,62 @@ impl InlineRule for Rule {
372372 }
373373 }
374374}
375
376#[cfg(test)]
377mod tests {
378 use super::find_same_line_close;
379 use crate::marko;
380
381 #[test]
382 fn test_find_same_line_close_with_nested_tags() {
383 // Should find </> after the nested <code></code>
384 let content = "<code>aaa</code> mmmm</>";
385 let result = find_same_line_close(content, "h3");
386 assert!(result.is_some(), "Should find closing tag");
387 let (before, close) = result.unwrap();
388 assert_eq!(before, "<code>aaa</code> mmmm");
389 assert_eq!(close, "</>");
390 }
391
392 #[test]
393 fn test_block_tag_with_inline_content() {
394 // the string
395 // 'bbbbbbb'><code>aaa</code> mmmm</>
396 //
397 // 'string' > <T>id < /regexp/ >
398 let src = "<h3 id='bbbbbbb'><code>aaa</code> mmmm</>";
399 let open = marko::parse_open(src).unwrap();
400 assert_eq!(open.tag_name(), "h3");
401 assert_eq!(open.src, "<h3 id='bbbbbbb'>");
402
403 // The rest after the tag
404 let rest = &src[open.src.len()..];
405 assert_eq!(rest, "<code>aaa</code> mmmm</>");
406
407 // find_same_line_close should find the </> at the end
408 let result = find_same_line_close(rest, "h3");
409 assert!(result.is_some(), "Should find closing tag for h3");
410 let (content, close) = result.unwrap();
411 assert_eq!(content, "<code>aaa</code> mmmm");
412 assert_eq!(close, "</>");
413 }
414
415 #[test]
416 fn test_nested_greater_than() {
417 let src = "<h3 id='bbbbbbb'><code>aaa</code> mmmm</>";
418 let open = marko::parse_open(src).unwrap();
419 assert_eq!(open.tag_name(), "h3");
420 assert_eq!(open.src, "<h3 id='bbbbbbb'>");
421
422 // The rest after the tag
423 let rest = &src[open.src.len()..];
424 assert_eq!(rest, "<code>aaa</code> mmmm</>");
425
426 // find_same_line_close should find the </> at the end
427 let result = find_same_line_close(rest, "h3");
428 assert!(result.is_some(), "Should find closing tag for h3");
429 let (content, close) = result.unwrap();
430 assert_eq!(content, "<code>aaa</code> mmmm");
431 assert_eq!(close, "</>");
432 }
433}
src/plugin/template.rs+2-1
......@@ -25,7 +25,8 @@ impl InlineRule for Rule {
2525 let allocator = Allocator::default();
2626 let source_type = SourceType::default()
2727 .with_module(true)
28 .with_typescript(true);
28 .with_typescript(true)
29 .with_jsx(false);
2930
3031 let expr = match Parser::new(&allocator, expr_source, source_type).parse_expression() {
3132 Ok(expr) => expr,
src/typescript.rs+65-9
......@@ -39,7 +39,8 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
3939
4040 let source_type = SourceType::default()
4141 .with_module(true)
42 .with_typescript(true);
42 .with_typescript(true)
43 .with_jsx(false);
4344
4445 let mut result = oxc_parser::Parser::new(allocator, source, source_type).parse();
4546 if !result.errors.is_empty() && result.program.body.is_empty() {
......@@ -111,7 +112,8 @@ pub fn parse_expr_extra<'alloc, 'src: 'alloc>(
111112
112113 let source_type = SourceType::default()
113114 .with_module(true)
114 .with_typescript(true);
115 .with_typescript(true)
116 .with_jsx(false);
115117
116118 let expr = match oxc_parser::Parser::new(allocator, source, source_type).parse_expression() {
117119 Ok(expr) => expr,
......@@ -185,25 +187,30 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
185187 let rest = l.peek_rest();
186188 let mut allocator = Allocator::new();
187189
188 // First, try parsing the full source and using AST walking to strip trailing `>`.
190 // First, try parsing the full source and using AST walking to find the earliest `>`.
189191 // This handles balanced cases like `arr[a > 0]` and `fn(a > b)` correctly.
192 // We walk down the left spine of binary expressions, looking for any `>` operator.
193 // When we find one, we record the span of its left operand. We keep walking to find
194 // the innermost `>` (which appears earliest in the source).
190195 if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) {
191196 let mut e = &expr;
197 let mut result_end = expr.span().end; // default: whole expression
198
192199 while let Expression::BinaryExpression(bin) = e {
193200 if bin.operator == BinaryOperator::GreaterThan
194201 || bin.operator == BinaryOperator::GreaterEqualThan
195202 || bin.operator == BinaryOperator::ShiftRight
196203 || bin.operator == BinaryOperator::ShiftRightZeroFill
197204 {
198 e = &bin.left;
199 } else {
200 break;
205 // Found a `>`, update result to be left side's span
206 result_end = bin.left.span().end;
201207 }
208 // Continue walking left to find any nested `>` operators
209 e = &bin.left;
202210 }
203211
204 let length = e.span().end;
205 l.advance(length);
206 return Ok(length);
212 l.advance(result_end);
213 return Ok(result_end);
207214 }
208215
209216 // If full parse failed (e.g., due to JSX-like content after the expression),
......@@ -871,4 +878,53 @@ mod tests {
871878 let result = scan_first_statement_forbid_trailing(source);
872879 assert!(result.is_ok());
873880 }
881
882 #[test]
883 fn test_no_gt_with_marko_tags_after() {
884 assert_eq!(parse_expr_no_gt("'test'>"), Ok(6));
885 assert_eq!(parse_expr_no_gt("'a'><b>"), Ok(3));
886 assert_eq!(parse_expr_no_gt("'x'><b/>"), Ok(3));
887 assert_eq!(parse_expr_no_gt("'x'><b></b>"), Ok(3));
888 assert_eq!(parse_expr_no_gt("'x'></>"), Ok(3));
889 assert_eq!(parse_expr_no_gt("'x'>text</>"), Ok(3));
890 assert_eq!(parse_expr_no_gt("'x'><a/></>"), Ok(3));
891 assert_eq!(parse_expr_no_gt("'x'><a></a></>"), Ok(3));
892 assert_eq!(parse_expr_no_gt("'x'><a>b</a></>"), Ok(3));
893 assert_eq!(parse_expr_no_gt("'x'><a>b</a> c</>"), Ok(3));
894 }
895
896 #[test]
897 fn test_no_gt_double_quoted_string() {
898 assert_eq!(parse_expr_no_gt("\"hello\">>><stuff>"), Ok(7));
899 }
900
901 #[test]
902 fn test_no_gt_string_followed_by_gt() {
903 // Simple string then >
904 assert_eq!(parse_expr_no_gt("'test'>"), Ok(6));
905 assert_eq!(parse_expr_no_gt("\"test\">"), Ok(6));
906 }
907
908 #[test]
909 fn test_no_gt_string_then_jsx_like() {
910 assert_eq!(parse_expr_no_gt("'a'><b>"), Ok(3)); // just 'a'
911 assert_eq!(parse_expr_no_gt("'a'>b"), Ok(3)); // just 'a'
912 }
913
914 #[test]
915 fn test_no_gt_string_then_jsx_variations() {
916 assert_eq!(parse_expr_no_gt("'x'><b>c</b>"), Ok(3));
917 assert_eq!(parse_expr_no_gt("'x'><b/>"), Ok(3));
918 assert_eq!(parse_expr_no_gt("'x'><b></b>"), Ok(3));
919 assert_eq!(parse_expr_no_gt("'x'><b>c</>"), Ok(3));
920 assert_eq!(parse_expr_no_gt("'x'><code>aaa</code>"), Ok(3));
921 }
922
923 #[test]
924 fn test_no_gt_other_ops() {
925 assert_eq!(parse_expr_no_gt("3 + 4 ___"), Ok(5));
926 assert_eq!(parse_expr_no_gt("3 + 4 > 4 ___"), Ok(5));
927 assert_eq!(parse_expr_no_gt("3 + 4 < 4 ___"), Ok(9));
928 assert_eq!(parse_expr_no_gt("3 < 4 ___"), Ok(5));
929 }
874930}