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 @@...@@ -1,6 +1,6 @@
1{1{
2 "name": "@clo/markodown",2 "name": "@clo/markodown",
3 "version": "1.0.0-rc.5",3 "version": "1.0.0-rc.6",
4 "license": "ISC",4 "license": "ISC",
5 "exports": {5 "exports": {
6 ".": "./mod.ts",6 ".": "./mod.ts",
src/outline.rs+1-1
...@@ -11,7 +11,7 @@ use std::collections::HashSet;...@@ -11,7 +11,7 @@ use std::collections::HashSet;
1111
12use crate::marko_ast::AttributeValue;12use crate::marko_ast::AttributeValue;
13use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText};13use crate::plugin::tags::{MarkoBlockComplete, MarkoOpen, MarkoOpenWithText};
14use crate::typescript::{parse_expr, parse_expr_extra};14use crate::typescript::parse_expr_extra;
1515
16/// A heading entry for the outline, with content reference for hoisting.16/// A heading entry for the outline, with content reference for hoisting.
17#[derive(Debug, Clone)]17#[derive(Debug, Clone)]
src/plugin/tags.rs+59
...@@ -372,3 +372,62 @@ impl InlineRule for Rule {...@@ -372,3 +372,62 @@ impl InlineRule for Rule {
372 }372 }
373 }373 }
374}374}
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 {...@@ -25,7 +25,8 @@ impl InlineRule for Rule {
25 let allocator = Allocator::default();25 let allocator = Allocator::default();
26 let source_type = SourceType::default()26 let source_type = SourceType::default()
27 .with_module(true)27 .with_module(true)
28 .with_typescript(true);28 .with_typescript(true)
29 .with_jsx(false);
2930
30 let expr = match Parser::new(&allocator, expr_source, source_type).parse_expression() {31 let expr = match Parser::new(&allocator, expr_source, source_type).parse_expression() {
31 Ok(expr) => expr,32 Ok(expr) => expr,
src/typescript.rs+65-9
...@@ -39,7 +39,8 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(...@@ -39,7 +39,8 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
3939
40 let source_type = SourceType::default()40 let source_type = SourceType::default()
41 .with_module(true)41 .with_module(true)
42 .with_typescript(true);42 .with_typescript(true)
43 .with_jsx(false);
4344
44 let mut result = oxc_parser::Parser::new(allocator, source, source_type).parse();45 let mut result = oxc_parser::Parser::new(allocator, source, source_type).parse();
45 if !result.errors.is_empty() && result.program.body.is_empty() {46 if !result.errors.is_empty() && result.program.body.is_empty() {
...@@ -111,7 +112,8 @@ pub fn parse_expr_extra<'alloc, 'src: 'alloc>(...@@ -111,7 +112,8 @@ pub fn parse_expr_extra<'alloc, 'src: 'alloc>(
111112
112 let source_type = SourceType::default()113 let source_type = SourceType::default()
113 .with_module(true)114 .with_module(true)
114 .with_typescript(true);115 .with_typescript(true)
116 .with_jsx(false);
115117
116 let expr = match oxc_parser::Parser::new(allocator, source, source_type).parse_expression() {118 let expr = match oxc_parser::Parser::new(allocator, source, source_type).parse_expression() {
117 Ok(expr) => expr,119 Ok(expr) => expr,
...@@ -185,25 +187,30 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<u32, OxcDiagnostic> {...@@ -185,25 +187,30 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
185 let rest = l.peek_rest();187 let rest = l.peek_rest();
186 let mut allocator = Allocator::new();188 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 `>`.
189 // This handles balanced cases like `arr[a > 0]` and `fn(a > b)` correctly.191 // 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).
190 if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) {195 if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) {
191 let mut e = &expr;196 let mut e = &expr;
197 let mut result_end = expr.span().end; // default: whole expression
198
192 while let Expression::BinaryExpression(bin) = e {199 while let Expression::BinaryExpression(bin) = e {
193 if bin.operator == BinaryOperator::GreaterThan200 if bin.operator == BinaryOperator::GreaterThan
194 || bin.operator == BinaryOperator::GreaterEqualThan201 || bin.operator == BinaryOperator::GreaterEqualThan
195 || bin.operator == BinaryOperator::ShiftRight202 || bin.operator == BinaryOperator::ShiftRight
196 || bin.operator == BinaryOperator::ShiftRightZeroFill203 || bin.operator == BinaryOperator::ShiftRightZeroFill
197 {204 {
198 e = &bin.left;205 // Found a `>`, update result to be left side's span
199 } else {206 result_end = bin.left.span().end;
200 break;
201 }207 }
208 // Continue walking left to find any nested `>` operators
209 e = &bin.left;
202 }210 }
203211
204 let length = e.span().end;212 l.advance(result_end);
205 l.advance(length);213 return Ok(result_end);
206 return Ok(length);
207 }214 }
208215
209 // If full parse failed (e.g., due to JSX-like content after the expression),216 // If full parse failed (e.g., due to JSX-like content after the expression),
...@@ -871,4 +878,53 @@ mod tests {...@@ -871,4 +878,53 @@ mod tests {
871 let result = scan_first_statement_forbid_trailing(source);878 let result = scan_first_statement_forbid_trailing(source);
872 assert!(result.is_ok());879 assert!(result.is_ok());
873 }880 }
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 }
874}930}