authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-13 11:04:50-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-13 11:09:29-08:00
loga4577db2eb8de0f1edf734aa8d476a349b7e8182
tree0c7a3389a40fd2100de62a5a8f435a4f06347067
parenta4a2b9e40b546078cae57f9ddc95701fc538419d
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

fix: parsing edge cases


6 files changed, 82 insertions(+), 14 deletions(-)

AGENTS.md+5
......@@ -15,6 +15,11 @@ be extremely hard to succeed.
1515- please maintain objective, high quality tests with `cargo test`
1616- never execute `git` commands. if you have broken your environment or are
1717 unsure how to continue, yield to the user.
18- if you are messing with the ts parsing helpers, remember to specifically care
19 about edge cases. you cannot write any manual lexer because you will almost
20 certainly implement it wrong (such as a brace close detection via depth
21 tracking). this is annoying but what we have to do since oxc doesn't expose
22 its internals. `typescript.rs` is full of clever solutions.
1823
1924## marko v6 tag syntax legend
2025
ARCHITECTURE.md+4
......@@ -2,6 +2,10 @@
22
33- `src/lib.rs` - main library, primary function `transform`
44- `src/main.rs` - main cli
5- `src/typescript.rs` - wrappers around oxc parser to expose parsers for
6 different subsets of the ast, including a much more garbage-forgiving
7 expression parser.
8- `src/marko.rs` - general marko tag parser
59- `src/plugin/` - `markdown-it` plugin and all rules
610
711markdown-it does not support failiable plugins, so instead errors are lowered
src/typescript.rs+46-12
......@@ -177,23 +177,57 @@ pub fn parse_expr(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
177177pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
178178 use oxc_ast::ast::{BinaryOperator, Expression};
179179
180 let rest = l.peek_rest();
180181 let mut allocator = Allocator::new();
181 let expr = parse_expr_extra(l.peek_rest(), l.offset().cast_signed(), &mut allocator)?;
182182
183 let mut e = &expr;
184 while let Expression::BinaryExpression(bin) = e {
185 if bin.operator == BinaryOperator::GreaterThan
186 || bin.operator == BinaryOperator::GreaterEqualThan
187 || bin.operator == BinaryOperator::ShiftRight
188 || bin.operator == BinaryOperator::ShiftRightZeroFill
189 {
190 e = &bin.left;
191 } else {
192 break;
183 // First, try parsing the full source and using AST walking to strip trailing `>`.
184 // This handles balanced cases like `arr[a > 0]` and `fn(a > b)` correctly.
185 if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) {
186 let mut e = &expr;
187 while let Expression::BinaryExpression(bin) = e {
188 if bin.operator == BinaryOperator::GreaterThan
189 || bin.operator == BinaryOperator::GreaterEqualThan
190 || bin.operator == BinaryOperator::ShiftRight
191 || bin.operator == BinaryOperator::ShiftRightZeroFill
192 {
193 e = &bin.left;
194 } else {
195 break;
196 }
197 }
198
199 let length = e.span().end as usize;
200 l.advance(length);
201 return Ok(length);
202 }
203
204 // If full parse failed (e.g., due to JSX-like content after the expression),
205 // try parsing prefixes ending at each `>` position.
206 let gt_positions: Vec<usize> = rest
207 .char_indices()
208 .filter(|(_, c)| *c == '>')
209 .map(|(i, _)| i)
210 .collect();
211
212 // Try from longest to shortest prefix, return first success
213 for &pos in gt_positions.iter().rev() {
214 let candidate = &rest[..pos];
215 allocator.reset();
216 if let Ok(expr) = parse_expr_extra(candidate, l.offset().cast_signed(), &mut allocator) {
217 // Check that the parse consumed most of the candidate (not just a prefix)
218 let length = expr.span().end as usize;
219 // If the expression spans close to the full candidate, use it
220 if length >= pos.saturating_sub(1) {
221 l.advance(length);
222 return Ok(length);
223 }
193224 }
194225 }
195226
196 let length = e.span().end as usize;
227 // Last resort: return error from the original parse attempt
228 allocator.reset();
229 let expr = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator)?;
230 let length = expr.span().end as usize;
197231 l.advance(length);
198232 Ok(length)
199233}
tests/fixtures.rs-2
......@@ -148,13 +148,11 @@ fn fixture_17_consecutive_statements() {
148148 run_fixture("17-consecutive-statements");
149149}
150150
151#[ignore = "known bug"]
152151#[test]
153152fn fixture_18_multiline_tag_with_value() {
154153 run_fixture("18-multiline-tag-with-value");
155154}
156155
157#[ignore = "known bug"]
158156#[test]
159157fn fixture_19_extra_cases() {
160158 run_fixture("19-extra-cases");
tests/fixtures/19-extra-cases.marko+12
......@@ -1,3 +1,15 @@
11<tag attr=(value
22>
335)>
4</>
5<tag attr=(value
6
7**content**
8
95
10
11>
12
135)>
14<p><strong>content</strong></p>
15</>
tests/fixtures/19-extra-cases.mdo+15
......@@ -2,3 +2,18 @@
22<tag attr=(value
33>
445)>
5</>
6
7<tag attr=(value
8
9**content**
10
115
12
13>
14
155)>
16
17**content**
18
19</>