Skip to content

Commit 05584fc

Browse files
committed
fix: regexes in frontmatter
"ç"
1 parent e039fcb commit 05584fc

1 file changed

Lines changed: 103 additions & 20 deletions

File tree

crates/oxc_parser/src/astro/parse.rs

Lines changed: 103 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,16 @@ fn scan_astro_frontmatter(source_text: &str) -> Option<AstroFrontmatterInfo> {
7676
Some(AstroFrontmatterInfo { content_start, content_end, frontmatter_end, body_start })
7777
}
7878

79-
/// Find the position of the closing `---` fence, skipping over strings, template literals, and comments.
79+
/// Find the position of the closing `---` fence, skipping over strings, template literals,
80+
/// comments, and regex literals.
8081
///
8182
/// This uses a simple state machine to track whether we're inside:
8283
/// - Single-quoted strings ('...')
8384
/// - Double-quoted strings ("...")
8485
/// - Template literals (`...`)
8586
/// - Line comments (// ...)
8687
/// - Block comments (/* ... */)
88+
/// - Regex literals (/.../)
8789
///
8890
/// Returns the byte offset of the closing `---` in the search area, or None if not found.
8991
fn find_closing_fence(search_area: &str) -> Option<usize> {
@@ -95,13 +97,23 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
9597
TemplateLiteral,
9698
LineComment,
9799
BlockComment,
100+
Regex,
101+
/// Inside a character class `[...]` within a regex literal.
102+
RegexCharClass,
98103
}
99104

100105
let bytes = search_area.as_bytes();
101106
let len = bytes.len();
102107
let mut state = State::Normal;
103108
let mut i = 0;
104109

110+
// Track whether a `/` should be interpreted as starting a regex literal vs division.
111+
// After these characters, `/` is the start of a regex literal:
112+
// punctuation/operators that cannot end an expression value.
113+
// After identifiers, numbers, `)`, `]`, `++`, `--`, `/` is division.
114+
// We track this with a simple flag: `true` means the next `/` could be a regex start.
115+
let mut slash_is_regex = true;
116+
105117
while i < len {
106118
let b = bytes[i];
107119

@@ -112,25 +124,50 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
112124
return Some(i);
113125
}
114126

115-
// Check for string/comment starts
127+
// Check for string/comment/regex starts
116128
match b {
117-
b'\'' => state = State::SingleQuote,
118-
b'"' => state = State::DoubleQuote,
119-
b'`' => state = State::TemplateLiteral,
120-
b'/' if i + 1 < len => {
121-
match bytes[i + 1] {
122-
b'/' => {
123-
state = State::LineComment;
124-
i += 1; // Skip the second '/'
125-
}
126-
b'*' => {
127-
state = State::BlockComment;
128-
i += 1; // Skip the '*'
129-
}
130-
_ => {}
131-
}
129+
b'\'' => {
130+
state = State::SingleQuote;
131+
slash_is_regex = true;
132+
}
133+
b'"' => {
134+
state = State::DoubleQuote;
135+
slash_is_regex = true;
136+
}
137+
b'`' => {
138+
state = State::TemplateLiteral;
139+
slash_is_regex = true;
140+
}
141+
b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
142+
state = State::LineComment;
143+
i += 1; // Skip the second '/'
144+
}
145+
b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
146+
state = State::BlockComment;
147+
i += 1; // Skip the '*'
148+
}
149+
b'/' if slash_is_regex => {
150+
state = State::Regex;
151+
}
152+
// Characters that indicate the next `/` is a regex (not division):
153+
// operators, punctuation, keywords that precede expressions
154+
b'=' | b'(' | b'[' | b'{' | b'}' | b';' | b',' | b'!' | b'&' | b'|' | b'^'
155+
| b'~' | b'?' | b':' | b'<' | b'>' | b'+' | b'-' | b'*' | b'%' | b'\n'
156+
| b'\r' => {
157+
slash_is_regex = true;
158+
}
159+
// After identifiers, numbers, `)`, `]`, `/` is division
160+
b')' | b']' => {
161+
slash_is_regex = false;
162+
}
163+
b if b.is_ascii_alphanumeric() || b == b'_' || b == b'$' => {
164+
slash_is_regex = false;
165+
}
166+
// Whitespace doesn't change the slash_is_regex flag
167+
b' ' | b'\t' => {}
168+
_ => {
169+
slash_is_regex = true;
132170
}
133-
_ => {}
134171
}
135172
}
136173

@@ -140,8 +177,12 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
140177
i += 1;
141178
} else if b == b'\'' {
142179
state = State::Normal;
180+
slash_is_regex = false;
181+
} else if b == b'\n' || b == b'\r' {
182+
// Newline ends single-quote string (syntax error in JS, but we recover)
183+
state = State::Normal;
184+
slash_is_regex = true;
143185
}
144-
// Note: newline ends single-quote string (syntax error in JS, but we continue)
145186
}
146187

147188
State::DoubleQuote => {
@@ -150,8 +191,12 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
150191
i += 1;
151192
} else if b == b'"' {
152193
state = State::Normal;
194+
slash_is_regex = false;
195+
} else if b == b'\n' || b == b'\r' {
196+
// Newline ends double-quote string (syntax error in JS, but we recover)
197+
state = State::Normal;
198+
slash_is_regex = true;
153199
}
154-
// Note: newline ends double-quote string (syntax error in JS, but we continue)
155200
}
156201

157202
State::TemplateLiteral => {
@@ -160,6 +205,7 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
160205
i += 1;
161206
} else if b == b'`' {
162207
state = State::Normal;
208+
slash_is_regex = false;
163209
}
164210
// Note: template literals CAN span multiple lines, so no newline handling
165211
// We also don't track ${...} interpolations - `---` inside interpolation
@@ -170,6 +216,7 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
170216
// Line comment ends at newline
171217
if b == b'\n' {
172218
state = State::Normal;
219+
slash_is_regex = true;
173220
}
174221
}
175222

@@ -178,6 +225,42 @@ fn find_closing_fence(search_area: &str) -> Option<usize> {
178225
if b == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
179226
state = State::Normal;
180227
i += 1; // Skip the '/'
228+
// Don't change slash_is_regex - preserve the context from before the comment
229+
}
230+
}
231+
232+
State::Regex => {
233+
if b == b'\\' && i + 1 < len {
234+
// Skip escaped character in regex
235+
i += 1;
236+
} else if b == b'[' {
237+
// Enter character class - `/` inside `[...]` doesn't end the regex
238+
state = State::RegexCharClass;
239+
} else if b == b'/' {
240+
// End of regex literal - skip optional flags (g, i, m, s, u, v, y, d)
241+
state = State::Normal;
242+
slash_is_regex = false;
243+
while i + 1 < len && bytes[i + 1].is_ascii_alphabetic() {
244+
i += 1;
245+
}
246+
} else if b == b'\n' || b == b'\r' {
247+
// Regex can't span lines - this was actually a division, recover
248+
state = State::Normal;
249+
slash_is_regex = true;
250+
}
251+
}
252+
253+
State::RegexCharClass => {
254+
if b == b'\\' && i + 1 < len {
255+
// Skip escaped character in character class
256+
i += 1;
257+
} else if b == b']' {
258+
// End of character class, back to regex body
259+
state = State::Regex;
260+
} else if b == b'\n' || b == b'\r' {
261+
// Regex can't span lines - recover
262+
state = State::Normal;
263+
slash_is_regex = true;
181264
}
182265
}
183266
}

0 commit comments

Comments
 (0)