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....@@ -15,6 +15,11 @@ be extremely hard to succeed.
15- please maintain objective, high quality tests with `cargo test`15- please maintain objective, high quality tests with `cargo test`
16- never execute `git` commands. if you have broken your environment or are16- never execute `git` commands. if you have broken your environment or are
17 unsure how to continue, yield to the user.17 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
19## marko v6 tag syntax legend24## marko v6 tag syntax legend
2025
ARCHITECTURE.md+4
...@@ -2,6 +2,10 @@...@@ -2,6 +2,10 @@
22
3- `src/lib.rs` - main library, primary function `transform`3- `src/lib.rs` - main library, primary function `transform`
4- `src/main.rs` - main cli4- `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
5- `src/plugin/` - `markdown-it` plugin and all rules9- `src/plugin/` - `markdown-it` plugin and all rules
610
7markdown-it does not support failiable plugins, so instead errors are lowered11markdown-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> {...@@ -177,23 +177,57 @@ pub fn parse_expr(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
177pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {177pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
178 use oxc_ast::ast::{BinaryOperator, Expression};178 use oxc_ast::ast::{BinaryOperator, Expression};
179179
180 let rest = l.peek_rest();
180 let mut allocator = Allocator::new();181 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;183 // First, try parsing the full source and using AST walking to strip trailing `>`.
184 while let Expression::BinaryExpression(bin) = e {184 // This handles balanced cases like `arr[a > 0]` and `fn(a > b)` correctly.
185 if bin.operator == BinaryOperator::GreaterThan185 if let Ok(expr) = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator) {
186 || bin.operator == BinaryOperator::GreaterEqualThan186 let mut e = &expr;
187 || bin.operator == BinaryOperator::ShiftRight187 while let Expression::BinaryExpression(bin) = e {
188 || bin.operator == BinaryOperator::ShiftRightZeroFill188 if bin.operator == BinaryOperator::GreaterThan
189 {189 || bin.operator == BinaryOperator::GreaterEqualThan
190 e = &bin.left;190 || bin.operator == BinaryOperator::ShiftRight
191 } else {191 || bin.operator == BinaryOperator::ShiftRightZeroFill
192 break;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 }
193 }224 }
194 }225 }
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;
197 l.advance(length);231 l.advance(length);
198 Ok(length)232 Ok(length)
199}233}
tests/fixtures.rs-2
...@@ -148,13 +148,11 @@ fn fixture_17_consecutive_statements() {...@@ -148,13 +148,11 @@ fn fixture_17_consecutive_statements() {
148 run_fixture("17-consecutive-statements");148 run_fixture("17-consecutive-statements");
149}149}
150150
151#[ignore = "known bug"]
152#[test]151#[test]
153fn fixture_18_multiline_tag_with_value() {152fn fixture_18_multiline_tag_with_value() {
154 run_fixture("18-multiline-tag-with-value");153 run_fixture("18-multiline-tag-with-value");
155}154}
156155
157#[ignore = "known bug"]
158#[test]156#[test]
159fn fixture_19_extra_cases() {157fn fixture_19_extra_cases() {
160 run_fixture("19-extra-cases");158 run_fixture("19-extra-cases");
tests/fixtures/19-extra-cases.marko+12
...@@ -1,3 +1,15 @@...@@ -1,3 +1,15 @@
1<tag attr=(value1<tag attr=(value
2>2>
35)>35)>
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 @@...@@ -2,3 +2,18 @@
2<tag attr=(value2<tag attr=(value
3>3>
45)>45)>
5</>
6
7<tag attr=(value
8
9**content**
10
115
12
13>
14
155)>
16
17**content**
18
19</>