From a4577db2eb8de0f1edf734aa8d476a349b7e8182 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 13 Feb 2026 11:04:50 -0800 Subject: [PATCH] fix: parsing edge cases --- AGENTS.md | 5 +++ ARCHITECTURE.md | 4 ++ src/typescript.rs | 58 +++++++++++++++++++++++------ tests/fixtures.rs | 2 - tests/fixtures/19-extra-cases.marko | 12 ++++++ tests/fixtures/19-extra-cases.mdo | 15 ++++++++ 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0d15a74723f47ad15db5ad7cba60df1c43fefd25..6b07a51e16bf24f303ff1812a753268b53e7d55f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,11 @@ be extremely hard to succeed. - please maintain objective, high quality tests with `cargo test` - never execute `git` commands. if you have broken your environment or are unsure how to continue, yield to the user. +- if you are messing with the ts parsing helpers, remember to specifically care + about edge cases. you cannot write any manual lexer because you will almost + certainly implement it wrong (such as a brace close detection via depth + tracking). this is annoying but what we have to do since oxc doesn't expose + its internals. `typescript.rs` is full of clever solutions. ## marko v6 tag syntax legend diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ae0181008c8fb65d8ad4bdcd0de7637948d691b9..52b8016b3b68e71e68f5495e6b720d1b9e079379 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,6 +2,10 @@ - `src/lib.rs` - main library, primary function `transform` - `src/main.rs` - main cli +- `src/typescript.rs` - wrappers around oxc parser to expose parsers for + different subsets of the ast, including a much more garbage-forgiving + expression parser. +- `src/marko.rs` - general marko tag parser - `src/plugin/` - `markdown-it` plugin and all rules markdown-it does not support failiable plugins, so instead errors are lowered diff --git a/src/typescript.rs b/src/typescript.rs index ca3281db3ab6b68132fbac2cfa2e3dba97d53765..9164fdd1b8020ddbbc936f485998f57c3bcb3d6a 100644 --- a/src/typescript.rs +++ b/src/typescript.rs @@ -177,23 +177,57 @@ pub fn parse_expr(l: &mut LexState) -> Result { pub fn parse_expr_without_gt(l: &mut LexState) -> Result { use oxc_ast::ast::{BinaryOperator, Expression}; + let rest = l.peek_rest(); let mut allocator = Allocator::new(); - let expr = parse_expr_extra(l.peek_rest(), l.offset().cast_signed(), &mut allocator)?; - let mut e = &expr; - 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; + // First, try parsing the full source and using AST walking to strip trailing `>`. + // This handles balanced cases like `arr[a > 0]` and `fn(a > b)` correctly. + if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) { + let mut e = &expr; + 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; + } + } + + let length = e.span().end as usize; + l.advance(length); + return Ok(length); + } + + // If full parse failed (e.g., due to JSX-like content after the expression), + // try parsing prefixes ending at each `>` position. + let gt_positions: Vec = rest + .char_indices() + .filter(|(_, c)| *c == '>') + .map(|(i, _)| i) + .collect(); + + // Try from longest to shortest prefix, return first success + for &pos in gt_positions.iter().rev() { + let candidate = &rest[..pos]; + allocator.reset(); + if let Ok(expr) = parse_expr_extra(candidate, l.offset().cast_signed(), &mut allocator) { + // Check that the parse consumed most of the candidate (not just a prefix) + let length = expr.span().end as usize; + // If the expression spans close to the full candidate, use it + if length >= pos.saturating_sub(1) { + l.advance(length); + return Ok(length); + } } } - let length = e.span().end as usize; + // Last resort: return error from the original parse attempt + allocator.reset(); + let expr = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator)?; + let length = expr.span().end as usize; l.advance(length); Ok(length) } diff --git a/tests/fixtures.rs b/tests/fixtures.rs index b71571a07019e283c3ffb44dc548a0068061e674..f53200711423139318deb8cd1de84926b70e41c3 100644 --- a/tests/fixtures.rs +++ b/tests/fixtures.rs @@ -148,13 +148,11 @@ fn fixture_17_consecutive_statements() { run_fixture("17-consecutive-statements"); } -#[ignore = "known bug"] #[test] fn fixture_18_multiline_tag_with_value() { run_fixture("18-multiline-tag-with-value"); } -#[ignore = "known bug"] #[test] fn fixture_19_extra_cases() { run_fixture("19-extra-cases"); diff --git a/tests/fixtures/19-extra-cases.marko b/tests/fixtures/19-extra-cases.marko index 8df27aa35e461d8ad2d621211d1d2197db7c0c60..0be72fb5945d47da0247596db08e1a6a35fb42a2 100644 --- a/tests/fixtures/19-extra-cases.marko +++ b/tests/fixtures/19-extra-cases.marko @@ -1,3 +1,15 @@ 5)> + + + +5)> +

content

+ diff --git a/tests/fixtures/19-extra-cases.mdo b/tests/fixtures/19-extra-cases.mdo index c112954d2d96388f67ff84ff0a153804b887b3f3..5b25811c1b36748bb8477a83f7efe0a4a2cb1336 100644 --- a/tests/fixtures/19-extra-cases.mdo +++ b/tests/fixtures/19-extra-cases.mdo @@ -2,3 +2,18 @@ 5)> + + + + +5)> + +**content** + + -- 2.54.0