authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-15 22:02:48-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-16 00:51:53-08:00
logcbd481161f7342c8238e2147f45e6048ad32ff4b
treebb95e6a77b5478e4cf7b351597820b882e08eb92
parentaca1b27d0ed5c6a73be9851f71733839cb7615f3
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

chore: don't be stupid regarding marko tags


11 files changed, 735 insertions(+), 858 deletions(-)

examples/marko-run/src/md/example.mdo-1
...@@ -28,7 +28,6 @@ a lot less brace hell for non-string attributes, and more treats!...@@ -28,7 +28,6 @@ a lot less brace hell for non-string attributes, and more treats!
28 <@img src="IMG_4838.jpeg" w=2 />28 <@img src="IMG_4838.jpeg" w=2 />
29 <@img src="IMG_4839.jpeg" w=2 align="top" />29 <@img src="IMG_4839.jpeg" w=2 align="top" />
30 <@img src="IMG_4833.jpeg" h=2 />30 <@img src="IMG_4833.jpeg" h=2 />
31 <@img src="IMG_4832.jpeg" w=2 />
32</>31</>
3332
34## in conclusion33## in conclusion
examples/marko-run/src/tags/photo-grid.marko+1-1
...@@ -46,7 +46,7 @@ export interface Input {...@@ -46,7 +46,7 @@ export interface Input {
46 "grid-template-rows": input.rows.map(row => `${row}px`).join(" "),46 "grid-template-rows": input.rows.map(row => `${row}px`).join(" "),
47}>47}>
48 <for|pos| of=computedPositions>48 <for|pos| of=computedPositions>
49 <const/itemStyle = `grid-column: ${pos.x + 1} / ${pos.x + 1 + pos.w}; grid-row: ${pos.y + 1} / ${pos.y + 1 + pos.h}; align-self: ${pos.align === "top" ? "start" : pos.align === "bottom" ? "end" : "center"};` />49 <const/itemStyle = `grid-column: ${pos.x + 1} / ${pos.x + 1 + pos.w}; grid-row: ${pos.y + 1} / ${pos.y + 1 + pos.h};` />
50 <const/hue = (pos.x + pos.y) * 37 % 360 />50 <const/hue = (pos.x + pos.y) * 37 % 360 />
51 <const/bgColor = `hsl(${hue}, 65%, 75%)` />51 <const/bgColor = `hsl(${hue}, 65%, 75%)` />
52 <div class="photo-grid-item" style=`${itemStyle} background: ${bgColor};` /> 52 <div class="photo-grid-item" style=`${itemStyle} background: ${bgColor};` />
src/lib.rs+46-82
...@@ -1,10 +1,11 @@...@@ -1,10 +1,11 @@
1pub mod marko;1pub mod marko;
2pub mod marko_ast;
2pub mod plugin;3pub mod plugin;
3pub mod typescript;4pub mod typescript;
4
5pub mod wasm;5pub mod wasm;
66
7use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};7use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
8use oxc_span::Span;
8use plugin::tags::{MarkoClose, MarkoOpen};9use plugin::tags::{MarkoClose, MarkoOpen};
9use serde::Serialize;10use serde::Serialize;
10use serde_json;11use serde_json;
...@@ -94,7 +95,7 @@ pub fn transform(...@@ -94,7 +95,7 @@ pub fn transform(
94 };95 };
9596
96 // Extract statements before rendering - they need to be hoisted above Layout97 // Extract statements before rendering - they need to be hoisted above Layout
97 let extracted_statements = extract_statements(&mut ast);98 let extracted_statements = hoist_statements(&mut ast);
9899
99 let mut text = ast.render();100 let mut text = ast.render();
100101
...@@ -177,9 +178,9 @@ pub fn adjust_err(mut err: OxcDiagnostic, offset: isize) -> OxcDiagnostic {...@@ -177,9 +178,9 @@ pub fn adjust_err(mut err: OxcDiagnostic, offset: isize) -> OxcDiagnostic {
177 err178 err
178}179}
179180
180/// Extract all StatementBlock content from the AST and clear them.181/// Extract `StatementBlock` contents from the AST, replacing them with an empty
181/// Returns the collected statement content as a single string.182/// string so they render as nothing. Returns the statements hoisted.
182fn extract_statements(node: &mut markdown_it::Node) -> String {183fn hoist_statements(node: &mut markdown_it::Node) -> String {
183 let mut statements = String::new();184 let mut statements = String::new();
184185
185 fn walk(node: &mut markdown_it::Node, statements: &mut String) {186 fn walk(node: &mut markdown_it::Node, statements: &mut String) {
...@@ -198,33 +199,15 @@ fn extract_statements(node: &mut markdown_it::Node) -> String {...@@ -198,33 +199,15 @@ fn extract_statements(node: &mut markdown_it::Node) -> String {
198199
199/// Check if the AST contains any Marko-specific features200/// Check if the AST contains any Marko-specific features
200fn has_marko_features(node: &markdown_it::Node) -> bool {201fn has_marko_features(node: &markdown_it::Node) -> bool {
201 // Check if this node is a RawBlock or StatementBlock202 node.cast::<plugin::RawBlock>().is_some()
202 if node.cast::<plugin::RawBlock>().is_some() {203 || node.cast::<plugin::StatementBlock>().is_some()
203 return true;204 || node.cast::<MarkoOpen>().is_some()
204 }
205 if node.cast::<plugin::StatementBlock>().is_some() {
206 return true;
207 }
208
209 // Check for Marko tags
210 if node.cast::<MarkoOpen>().is_some()
211 || node.cast::<MarkoClose>().is_some()205 || node.cast::<MarkoClose>().is_some()
212 || node.cast::<plugin::tags::MarkoSelfClosing>().is_some()206 || node.cast::<plugin::tags::MarkoSelfClosing>().is_some()
213 || node.cast::<plugin::tags::MarkoOpenWithText>().is_some()207 || node.cast::<plugin::tags::MarkoOpenWithText>().is_some()
214 || node.cast::<plugin::tags::MarkoInlineTag>().is_some()208 || node.cast::<plugin::tags::MarkoInlineTag>().is_some()
215 || node.cast::<plugin::tags::MarkoBlockComplete>().is_some()209 || node.cast::<plugin::tags::MarkoBlockComplete>().is_some()
216 {210 || node.children.iter().any(|child| has_marko_features(child))
217 return true;
218 }
219
220 // Recursively check children
221 for child in &node.children {
222 if has_marko_features(child) {
223 return true;
224 }
225 }
226
227 false
228}211}
229212
230/// Validate that Marko open/close tags are properly matched213/// Validate that Marko open/close tags are properly matched
...@@ -233,68 +216,57 @@ fn validate_marko_tags(...@@ -233,68 +216,57 @@ fn validate_marko_tags(
233 errors: &mut Vec<OxcDiagnostic>,216 errors: &mut Vec<OxcDiagnostic>,
234 preamble_offset: usize,217 preamble_offset: usize,
235) {218) {
236 #[derive(Debug)]219 // Stack of (tag_name, absolute_span_of_name)
237 struct OpenTag {220 let mut stack: Vec<(String, Span)> = vec![];
238 name: String,
239 /// Byte offset of the tag name in the source (after '<')
240 name_start: usize,
241 /// Length of the tag name
242 name_len: usize,
243 }
244
245 let mut stack: Vec<OpenTag> = vec![];
246221
247 fn walk(222 fn walk(
248 node: &markdown_it::Node,223 node: &markdown_it::Node,
249 stack: &mut Vec<OpenTag>,224 stack: &mut Vec<(String, Span)>,
250 errors: &mut Vec<OxcDiagnostic>,225 errors: &mut Vec<OxcDiagnostic>,
251 offset: usize,226 offset: u32,
252 ) {227 ) {
253 if let Some(open) = node.cast::<MarkoOpen>() {228 if let Some(open) = node.cast::<MarkoOpen>() {
254 let (start, _) = node.srcmap.unwrap().get_byte_offsets();229 let (start, _) = node.srcmap.unwrap().get_byte_offsets();
255 stack.push(OpenTag {230 let rel_span = open.open.as_ref().tag_name_span();
256 name: open.tag_name.clone(),231 let abs_span = Span::new(
257 name_start: start + offset + 1, // +1 to skip '<'232 start as u32 + offset + rel_span.start,
258 name_len: open.tag_name_len,233 start as u32 + offset + rel_span.end,
259 });234 );
235 stack.push((open.open.as_ref().tag_name().to_owned(), abs_span));
260 } else if let Some(open) = node.cast::<plugin::tags::MarkoOpenWithText>() {236 } else if let Some(open) = node.cast::<plugin::tags::MarkoOpenWithText>() {
261 let (start, _) = node.srcmap.unwrap().get_byte_offsets();237 let (start, _) = node.srcmap.unwrap().get_byte_offsets();
262 stack.push(OpenTag {238 let rel_span = open.open.as_ref().tag_name_span();
263 name: open.tag_name.clone(),239 let abs_span = Span::new(
264 name_start: start + offset + 1, // +1 to skip '<'240 start as u32 + offset + rel_span.start,
265 name_len: open.tag_name_len,241 start as u32 + offset + rel_span.end,
266 });242 );
243 stack.push((open.open.as_ref().tag_name().to_owned(), abs_span));
267 } else if let Some(close) = node.cast::<MarkoClose>() {244 } else if let Some(close) = node.cast::<MarkoClose>() {
268 let (close_start, _) = node.srcmap.unwrap().get_byte_offsets();245 let (close_start, _) = node.srcmap.unwrap().get_byte_offsets();
246 let close_start = close_start as u32 + offset;
269 // Close tag name starts after '</' (offset +2)247 // Close tag name starts after '</' (offset +2)
270 let close_name_start = close_start + offset + 2;248 let close_span = close
271 let close_name_len = close.tag_name_len.unwrap_or(0);249 .tag_name
250 .as_ref()
251 .map(|name| Span::new(close_start + 2, close_start + 2 + name.len() as u32));
272252
273 match (close.tag_name.as_ref(), stack.pop()) {253 match (close.tag_name.as_ref(), stack.pop()) {
254 (Some(name), Some((top_name, _))) if name == &top_name => {
255 // valid
256 }
274 (None, Some(_)) => {257 (None, Some(_)) => {
275 // </> closes anything - valid258 // </> closes anything - valid
276 }259 }
277 (Some(name), Some(top)) if name == &top.name => {260 (Some(name), Some((top_name, top_span))) => {
278 // Exact match - valid261 // Mismatched: expected </top_name>, got </name>
279 }
280 (Some(name), Some(top)) => {
281 // Mismatched: expected </top.name>, got </name>
282 errors.push(262 errors.push(
283 OxcDiagnostic::error(format!(263 OxcDiagnostic::error(format!(
284 "Mismatched closing tag: expected </{}>, found </{}>",264 "Mismatched closing tag: expected </{}>, found </{}>",
285 top.name, name265 top_name, name
286 ))266 ))
287 .with_labels(vec![267 .with_labels(vec![
288 LabeledSpan::new(268 top_span.label("opened here"),
289 Some("opened here".into()),269 close_span.unwrap().label("closed here"),
290 top.name_start,
291 top.name_len,
292 ),
293 LabeledSpan::new(
294 Some("closed here".into()),
295 close_name_start,
296 close_name_len,
297 ),
298 ]),270 ]),
299 );271 );
300 }272 }
...@@ -304,18 +276,14 @@ fn validate_marko_tags(...@@ -304,18 +276,14 @@ fn validate_marko_tags(
304 OxcDiagnostic::error(format!(276 OxcDiagnostic::error(format!(
305 "Closing tag </{name}> without matching open"277 "Closing tag </{name}> without matching open"
306 ))278 ))
307 .with_label(LabeledSpan::new(279 .with_label(close_span.unwrap()),
308 None,
309 close_name_start,
310 close_name_len,
311 )),
312 );280 );
313 }281 }
314 (None, None) => {282 (None, None) => {
315 // </> without any open tag - highlight the whole </>283 // </> without any open tag - highlight the whole </>
316 errors.push(284 errors.push(
317 OxcDiagnostic::error("Closing tag </> without matching open")285 OxcDiagnostic::error("Closing tag </> without matching open")
318 .with_label(LabeledSpan::new(None, close_start + offset, 3)),286 .with_label(Span::new(close_start, close_start + 3)),
319 );287 );
320 }288 }
321 }289 }
...@@ -327,18 +295,14 @@ fn validate_marko_tags(...@@ -327,18 +295,14 @@ fn validate_marko_tags(
327 }295 }
328 }296 }
329297
330 walk(node, &mut stack, errors, preamble_offset);298 walk(node, &mut stack, errors, preamble_offset as u32);
331299
332 // Check for unclosed tags300 // Check for unclosed tags
333 for unclosed in stack {301 for (name, span) in stack {
334 errors.push(302 errors.push(OxcDiagnostic::error(format!("Unclosed tag <{}>", name)).with_label(span));
335 OxcDiagnostic::error(format!("Unclosed tag <{}>", unclosed.name)).with_label(
336 LabeledSpan::new(None, unclosed.name_start, unclosed.name_len),
337 ),
338 );
339 }303 }
340}304}
341305
342pub fn err<T: Into<Cow<'static, str>>>(str: T, offset: usize, length: usize) -> OxcDiagnostic {306pub fn err<T: Into<Cow<'static, str>>>(str: T, offset: u32, length: usize) -> OxcDiagnostic {
343 OxcDiagnostic::error(str).and_label(LabeledSpan::new(None, offset, length))307 OxcDiagnostic::error(str).and_label(LabeledSpan::new(None, offset as usize, length))
344}308}
src/marko.rs+334-629
...@@ -1,74 +1,16 @@...@@ -1,74 +1,16 @@
1use std::{borrow::Cow, iter::Map};
2
3use oxc_diagnostics::OxcDiagnostic;1use oxc_diagnostics::OxcDiagnostic;
4use oxc_span::Span;2use oxc_span::Span;
3use serde_yml::libyml::tag;
54
6use crate::{5use crate::{
7 err,6 err,
7 marko_ast::*,
8 typescript::{8 typescript::{
9 parse_call_arguments, parse_expr, parse_expr_without_gt, parse_fn_params_and_body,9 parse_call_arguments, parse_expr, parse_expr_without_gt, parse_fn_params_and_body,
10 parse_var_binding,10 parse_var_binding,
11 },11 },
12};12};
1313
14#[derive(Debug, Clone, PartialEq)]
15pub struct Open<'a> {
16 pub tag_name: &'a str,
17 pub content: &'a str,
18 pub self_closing: bool,
19}
20
21impl<'a> Open<'a> {
22 /// Returns the span of the tag name relative to the start of the tag.
23 pub fn tag_name_span(&self) -> Span {
24 // fast case, when a literal tag name is passed
25 if !self.tag_name.is_empty() {
26 Span::new(1, 1 + (self.tag_name.len() as u32))
27 } else {
28 // slow path, re-parse the js expression. not that deep because you
29 // only do this in the error case. TODO: actually store `Open` in
30 // the md tree, the lifetimes are complicated rn
31 let mut l = LexState::new(self.content);
32 l.advance(3);
33 parse_expr(&mut l).expect("validated text should pass");
34 Span::new(3, l.offset() as u32)
35 }
36 }
37
38 pub fn id_attr(&self) -> Option<&'a str> {
39 todo!();
40 }
41
42 pub fn set_id_attr(&mut self, str: &'a str) {
43 todo!();
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Close<'a> {
49 pub tag_name: Option<&'a str>,
50 pub length: usize,
51}
52
53impl Close<'_> {
54 /// Returns the span of the tag name relative to the start of the tag.
55 /// For `</div>`, returns Span(2, 5) pointing to "div".
56 /// For `</>`, returns None.
57 pub fn tag_name_span(&self) -> Option<Span> {
58 self.tag_name.map(|name| {
59 // Close tag starts with '</', so tag name starts at offset 2
60 let start = 2u32;
61 let end = start + (name.len() as u32);
62 Span::new(start, end)
63 })
64 }
65}
66
67pub enum OpenOrClose<'a> {
68 Open(Open<'a>),
69 Close(Close<'a>),
70}
71
72pub fn parse_tag(src: &str) -> Result<OpenOrClose, OxcDiagnostic> {14pub fn parse_tag(src: &str) -> Result<OpenOrClose, OxcDiagnostic> {
73 assert_eq!(src.as_bytes()[0], b'<');15 assert_eq!(src.as_bytes()[0], b'<');
74 if src.starts_with("</") {16 if src.starts_with("</") {
...@@ -78,66 +20,6 @@ pub fn parse_tag(src: &str) -> Result<OpenOrClose, OxcDiagnostic> {...@@ -78,66 +20,6 @@ pub fn parse_tag(src: &str) -> Result<OpenOrClose, OxcDiagnostic> {
78 }20 }
79}21}
8022
81pub struct LexState<'a> {
82 pub src: &'a str,
83 pub offset: usize,
84}
85
86impl<'a> LexState<'a> {
87 pub fn new(src: &'a str) -> LexState<'a> {
88 LexState { src, offset: 0 }
89 }
90
91 pub fn offset(&self) -> usize {
92 self.offset
93 }
94
95 pub fn advance(&mut self, n: usize) {
96 self.offset += n;
97 }
98
99 pub fn restore(&mut self, n: usize) {
100 self.offset = n;
101 }
102
103 pub fn peek_byte(&self) -> Option<u8> {
104 self.src.as_bytes().get(self.offset).copied()
105 }
106
107 pub fn peek_rest(&self) -> &'a str {
108 &self.src[self.offset..]
109 }
110
111 pub fn expect_byte(&mut self) -> Result<u8, OxcDiagnostic> {
112 if let Some(b) = self.peek_byte() {
113 self.offset += 1;
114 return Ok(b);
115 }
116 Err(err("Unexpected end of file", self.offset, 1))
117 }
118
119 pub fn expect(&mut self, expected: &str) -> Result<(), OxcDiagnostic> {
120 if self.src[self.offset..].starts_with(expected) {
121 self.offset += expected.len();
122 Ok(())
123 } else {
124 Err(err(format!("Expected {expected}"), self.offset, 1))
125 }
126 }
127
128 pub fn skip_whitespace(&mut self) {
129 loop {
130 match self.peek_byte() {
131 Some(byte) => match byte {
132 b' ' | b'\n' | b'\t' | b'\r' => self.offset += 1,
133 _ => break,
134 },
135 None => break,
136 }
137 }
138 }
139}
140
141pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {23pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
142 assert!(src.starts_with("<"));24 assert!(src.starts_with("<"));
143 let mut l = LexState { src, offset: 1 };25 let mut l = LexState { src, offset: 1 };
...@@ -145,21 +27,33 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {...@@ -145,21 +27,33 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
145 let is_attribute_tag = tag_name.starts_with('@');27 let is_attribute_tag = tag_name.starts_with('@');
14628
147 // class and id shorthand29 // class and id shorthand
148 let mut has_id = false;30 let mut id_attr = AttributeValue::None;
149 while let Some(byte @ b'#' | byte @ b'.') = l.peek_byte() {31 while let Some(byte @ b'#' | byte @ b'.') = l.peek_byte() {
150 let offset = l.offset;32 let id_start = l.offset;
151 l.advance(1);33 l.advance(1);
152 let name = parse_class_name_shorthand(&mut l, byte == b'#')?;34 let name = parse_class_name_shorthand(&mut l, byte == b'#')?;
153 _ = name;35 let id_end = l.offset;
15436
155 if byte == b'#' {37 if byte == b'#' {
156 if has_id {38 if !matches!(id_attr, AttributeValue::None) {
157 return Err(err("Cannot specify two ID shorthands", offset, name.len()));39 return Err(err("Cannot specify two IDs", id_start, name.len()));
40 }
41
42 // Check if the shorthand contains ${} (dynamic)
43 if name.contains("${") {
44 id_attr = AttributeValue::Dynamic;
45 } else {
46 id_attr = AttributeValue::Static {
47 span: Span::new(id_start + 1, id_end),
48 is_quoted: false,
49 };
158 }50 }
159 has_id = true;
160 }51 }
161 }52 }
16253
54 // Track where shorthands end (for inserting new #id if needed)
55 let shorthand_end = l.offset;
56
163 // in any order: parameters, arguments, variable57 // in any order: parameters, arguments, variable
164 let mut has_js_arguments = false;58 let mut has_js_arguments = false;
165 let mut has_js_params = false;59 let mut has_js_params = false;
...@@ -239,14 +133,14 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {...@@ -239,14 +133,14 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
239 return Err(err(133 return Err(err(
240 "Attribute tags do not support variables",134 "Attribute tags do not support variables",
241 offset,135 offset,
242 l.offset - offset,136 (l.offset - offset) as usize,
243 ));137 ));
244 }138 }
245 if is_attribute_tag && byte == b'(' {139 if is_attribute_tag && byte == b'(' {
246 return Err(err(140 return Err(err(
247 "Attribute tags do not support arguments",141 "Attribute tags do not support arguments",
248 offset,142 offset,
249 l.offset - offset,143 (l.offset - offset) as usize,
250 ));144 ));
251 }145 }
252 }146 }
...@@ -274,8 +168,36 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {...@@ -274,8 +168,36 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
274 self_closing = true;168 self_closing = true;
275 break;169 break;
276 }170 }
277 parse_attribute(&mut l)?;171 // Check for id= attribute (only if we don't already have an id)
278 l.skip_whitespace();172 if l.peek_rest().starts_with("id=") {
173 if !matches!(id_attr, AttributeValue::None) {
174 return Err(err(
175 "Cannot combine the 'id' attribute with ID shorthand",
176 l.offset(),
177 3,
178 ));
179 }
180
181 l.advance(3); // skip "id="
182 l.skip_whitespace();
183 let value_start = l.offset;
184 let first_byte = l.peek_byte();
185 parse_expr_without_gt(&mut l)?;
186 let value_end = l.offset;
187 l.skip_whitespace();
188
189 if first_byte == Some(b'"') || first_byte == Some(b'\'') {
190 id_attr = AttributeValue::Static {
191 span: Span::new(value_start, value_end),
192 is_quoted: true,
193 };
194 } else {
195 id_attr = AttributeValue::Dynamic;
196 }
197 } else {
198 parse_attribute(&mut l)?;
199 l.skip_whitespace();
200 }
279 }201 }
280 }202 }
281 }203 }
...@@ -287,11 +209,13 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {...@@ -287,11 +209,13 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
287 }209 }
288 }210 }
289211
290 Ok(Open {212 Ok(Open::new(
291 tag_name,213 &src[0..l.offset as usize],
292 content: &src[0..l.offset],214 Span::new(1, (tag_name.len() + 1) as u32),
293 self_closing,215 self_closing,
294 })216 shorthand_end,
217 id_attr,
218 ))
295}219}
296220
297pub fn parse_close(src: &str) -> Result<Close, OxcDiagnostic> {221pub fn parse_close(src: &str) -> Result<Close, OxcDiagnostic> {
...@@ -329,7 +253,7 @@ fn parse_tag_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> {...@@ -329,7 +253,7 @@ fn parse_tag_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> {
329 _ => break,253 _ => break,
330 }254 }
331 }255 }
332 let tag_name = &l.src[start..l.offset];256 let tag_name = &l.src[start as usize..l.offset as usize];
333 if !tag_name.is_empty() {257 if !tag_name.is_empty() {
334 Ok(tag_name)258 Ok(tag_name)
335 } else {259 } else {
...@@ -345,7 +269,7 @@ fn parse_attr_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> {...@@ -345,7 +269,7 @@ fn parse_attr_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> {
345 _ => break,269 _ => break,
346 }270 }
347 }271 }
348 let tag_name = &l.src[start..l.offset];272 let tag_name = &l.src[start as usize..l.offset as usize];
349 if !tag_name.is_empty() {273 if !tag_name.is_empty() {
350 Ok(tag_name)274 Ok(tag_name)
351 } else {275 } else {
...@@ -408,7 +332,7 @@ fn parse_class_name_shorthand<'a>(...@@ -408,7 +332,7 @@ fn parse_class_name_shorthand<'a>(
408 _ => l.offset += 1,332 _ => l.offset += 1,
409 }333 }
410 }334 }
411 let tag_name = &l.src[start..l.offset];335 let tag_name = &l.src[start as usize..l.offset as usize];
412 if !tag_name.is_empty() {336 if !tag_name.is_empty() {
413 Ok(tag_name)337 Ok(tag_name)
414 } else {338 } else {
...@@ -428,133 +352,73 @@ fn parse_class_name_shorthand<'a>(...@@ -428,133 +352,73 @@ fn parse_class_name_shorthand<'a>(
428mod tests {352mod tests {
429 use super::*;353 use super::*;
430354
431 #[test]355 /// Helper to check basic Open properties without worrying about id_info/shorthand_end
432 fn test_open_basic() {356 fn check_open(src: &str, tag_name: &str, content: &str, self_closing: bool) {
357 let open = parse_open(src).unwrap();
358 assert_eq!(open.tag_name(), tag_name, "tag_name mismatch for {src}");
359 assert_eq!(open.src, content, "content mismatch for {src}");
433 assert_eq!(360 assert_eq!(
434 parse_open("<div>"),361 open.self_closing, self_closing,
435 Ok(Open {362 "self_closing mismatch for {src}"
436 tag_name: "div",
437 content: "<div>",
438 self_closing: false,
439 })
440 );363 );
441 assert_eq!(364 }
442 parse_open("<footer#id.class meow=purr yolo=4.2 /> etfdas"),365
443 Ok(Open {366 #[test]
444 tag_name: "footer",367 fn test_open_basic() {
445 content: "<footer#id.class meow=purr yolo=4.2 />",368 check_open("<div>", "div", "<div>", false);
446 self_closing: true,369 check_open(
447 })370 "<footer#id.class meow=purr yolo=4.2 /> etfdas",
371 "footer",
372 "<footer#id.class meow=purr yolo=4.2 />",
373 true,
448 );374 );
449 assert_eq!(375 check_open(
450 parse_open("<movie value=x > 4 /> etfdas"),376 "<movie value=x > 4 /> etfdas",
451 Ok(Open {377 "movie",
452 tag_name: "movie",378 "<movie value=x >",
453 content: "<movie value=x >",379 false,
454 self_closing: false,
455 })
456 );380 );
457 }381 }
458382
459 #[test]383 #[test]
460 fn test_open_id() {384 fn test_open_id() {
461 assert_eq!(385 check_open(
462 parse_open("<h2#technical-review>A Technical Review"),386 "<h2#technical-review>A Technical Review",
463 Ok(Open {387 "h2",
464 tag_name: "h2",388 "<h2#technical-review>",
465 content: "<h2#technical-review>",389 false,
466 self_closing: false,
467 })
468 );390 );
469 assert_eq!(391 check_open(
470 parse_open("<footer#id.class meow=purr yolo=4.2 /> etfdas"),392 "<footer#id.class meow=purr yolo=4.2 /> etfdas",
471 Ok(Open {393 "footer",
472 tag_name: "footer",394 "<footer#id.class meow=purr yolo=4.2 />",
473 content: "<footer#id.class meow=purr yolo=4.2 />",395 true,
474 self_closing: true,
475 })
476 );396 );
477 assert_eq!(397 check_open(
478 parse_open("<movie value=x > 4 /> etfdas"),398 "<movie value=x > 4 /> etfdas",
479 Ok(Open {399 "movie",
480 tag_name: "movie",400 "<movie value=x >",
481 content: "<movie value=x >",401 false,
482 self_closing: false,
483 })
484 );402 );
485 }403 }
486404
487 #[test]405 #[test]
488 fn test_open_self_closing_with_space() {406 fn test_open_self_closing_with_space() {
489 // Self-closing with space before /> works407 check_open("<input />", "input", "<input />", true);
490 assert_eq!(
491 parse_open("<input />"),
492 Ok(Open {
493 tag_name: "input",
494 content: "<input />",
495 self_closing: true,
496 })
497 );
498 }408 }
499409
500 #[test]410 #[test]
501 fn test_open_self_closing_no_space() {411 fn test_open_self_closing_no_space() {
502 assert_eq!(412 check_open("<br/>", "br", "<br/>", true);
503 parse_open("<br/>"),413 check_open("<my-component/>", "my-component", "<my-component/>", true);
504 Ok(Open {
505 tag_name: "br",
506 content: "<br/>",
507 self_closing: true,
508 })
509 );
510 assert_eq!(
511 parse_open("<my-component/>"),
512 Ok(Open {
513 tag_name: "my-component",
514 content: "<my-component/>",
515 self_closing: true,
516 })
517 );
518 }414 }
519415
520 #[test]416 #[test]
521 fn test_open_tag_names() {417 fn test_open_tag_names() {
522 // kebab-case418 check_open("<my-custom-tag>", "my-custom-tag", "<my-custom-tag>", false);
523 assert_eq!(419 check_open("<MyComponent>", "MyComponent", "<MyComponent>", false);
524 parse_open("<my-custom-tag>"),420 check_open("<my_tag>", "my_tag", "<my_tag>", false);
525 Ok(Open {421 check_open("<$tag>", "$tag", "<$tag>", false);
526 tag_name: "my-custom-tag",
527 content: "<my-custom-tag>",
528 self_closing: false,
529 })
530 );
531 // PascalCase (for custom tags referencing imports)
532 assert_eq!(
533 parse_open("<MyComponent>"),
534 Ok(Open {
535 tag_name: "MyComponent",
536 content: "<MyComponent>",
537 self_closing: false,
538 })
539 );
540 // with underscore
541 assert_eq!(
542 parse_open("<my_tag>"),
543 Ok(Open {
544 tag_name: "my_tag",
545 content: "<my_tag>",
546 self_closing: false,
547 })
548 );
549 // with $ (valid in Marko)
550 assert_eq!(
551 parse_open("<$tag>"),
552 Ok(Open {
553 tag_name: "$tag",
554 content: "<$tag>",
555 self_closing: false,
556 })
557 );
558 }422 }
559423
560 #[test]424 #[test]
...@@ -602,53 +466,32 @@ mod tests {...@@ -602,53 +466,32 @@ mod tests {
602466
603 #[test]467 #[test]
604 fn test_attribute_tag_basic_open() {468 fn test_attribute_tag_basic_open() {
605 // @attr tag without self-closing works469 check_open("<@header>", "@header", "<@header>", false);
606 assert_eq!(
607 parse_open("<@header>"),
608 Ok(Open {
609 tag_name: "@header",
610 content: "<@header>",
611 self_closing: false,
612 })
613 );
614 }470 }
615471
616 #[test]472 #[test]
617 fn test_attribute_tag_self_closing() {473 fn test_attribute_tag_self_closing() {
618 assert_eq!(474 check_open("<@item/>", "@item", "<@item/>", true);
619 parse_open("<@item/>"),
620 Ok(Open {
621 tag_name: "@item",
622 content: "<@item/>",
623 self_closing: true,
624 })
625 );
626 }475 }
627476
628 #[test]477 #[test]
629 fn test_attribute_tag_with_string_attr() {478 fn test_attribute_tag_with_string_attr() {
630 assert_eq!(479 check_open(
631 parse_open("<@option value=\"foo\">"),480 "<@option value=\"foo\">",
632 Ok(Open {481 "@option",
633 tag_name: "@option",482 "<@option value=\"foo\">",
634 content: "<@option value=\"foo\">",483 false,
635 self_closing: false,
636 })
637 );484 );
638 }485 }
639486
640 #[test]487 #[test]
641 fn test_attribute_tag_no_variable() {488 fn test_attribute_tag_no_variable() {
642 // Attribute tags do not support tag variables - correctly errors489 assert!(parse_open("<@header/myVar>").is_err());
643 let result = parse_open("<@header/myVar>");
644 assert!(result.is_err());
645 }490 }
646491
647 #[test]492 #[test]
648 fn test_attribute_tag_no_arguments() {493 fn test_attribute_tag_no_arguments() {
649 // Attribute tags do not support tag arguments - correctly errors494 assert!(parse_open("<@header(arg)>").is_err());
650 let result = parse_open("<@header(arg)>");
651 assert!(result.is_err());
652 }495 }
653496
654 // ===========================================497 // ===========================================
...@@ -657,97 +500,47 @@ mod tests {...@@ -657,97 +500,47 @@ mod tests {
657500
658 #[test]501 #[test]
659 fn test_shorthand_id() {502 fn test_shorthand_id() {
660 assert_eq!(503 check_open("<div#myId>", "div", "<div#myId>", false);
661 parse_open("<div#myId>"),
662 Ok(Open {
663 tag_name: "div",
664 content: "<div#myId>",
665 self_closing: false,
666 })
667 );
668 }504 }
669505
670 #[test]506 #[test]
671 fn test_shorthand_class() {507 fn test_shorthand_class() {
672 assert_eq!(508 check_open("<div.myClass>", "div", "<div.myClass>", false);
673 parse_open("<div.myClass>"),
674 Ok(Open {
675 tag_name: "div",
676 content: "<div.myClass>",
677 self_closing: false,
678 })
679 );
680 }509 }
681510
682 #[test]511 #[test]
683 fn test_shorthand_multiple_classes() {512 fn test_shorthand_multiple_classes() {
684 assert_eq!(513 check_open("<div.cls1.cls2.cls3>", "div", "<div.cls1.cls2.cls3>", false);
685 parse_open("<div.cls1.cls2.cls3>"),
686 Ok(Open {
687 tag_name: "div",
688 content: "<div.cls1.cls2.cls3>",
689 self_closing: false,
690 })
691 );
692 }514 }
693515
694 #[test]516 #[test]
695 fn test_shorthand_id_and_classes() {517 fn test_shorthand_id_and_classes() {
696 assert_eq!(518 check_open("<div#myId.cls1.cls2>", "div", "<div#myId.cls1.cls2>", false);
697 parse_open("<div#myId.cls1.cls2>"),
698 Ok(Open {
699 tag_name: "div",
700 content: "<div#myId.cls1.cls2>",
701 self_closing: false,
702 })
703 );
704 }519 }
705520
706 #[test]521 #[test]
707 fn test_shorthand_classes_then_id() {522 fn test_shorthand_classes_then_id() {
708 // Order shouldn't matter523 check_open("<div.cls1#myId.cls2>", "div", "<div.cls1#myId.cls2>", false);
709 assert_eq!(
710 parse_open("<div.cls1#myId.cls2>"),
711 Ok(Open {
712 tag_name: "div",
713 content: "<div.cls1#myId.cls2>",
714 self_closing: false,
715 })
716 );
717 }524 }
718525
719 #[test]526 #[test]
720 fn test_shorthand_duplicate_id_error() {527 fn test_shorthand_duplicate_id_error() {
721 // Cannot have two ID shorthands - correctly errors528 assert!(parse_open("<div#id1#id2>").is_err());
722 let result = parse_open("<div#id1#id2>");
723 assert!(result.is_err());
724 }529 }
725530
726 #[test]531 #[test]
727 fn test_shorthand_with_hyphen() {532 fn test_shorthand_with_hyphen() {
728 // Classes/IDs can contain hyphens533 check_open("<div.my-class#my-id>", "div", "<div.my-class#my-id>", false);
729 assert_eq!(
730 parse_open("<div.my-class#my-id>"),
731 Ok(Open {
732 tag_name: "div",
733 content: "<div.my-class#my-id>",
734 self_closing: false,
735 })
736 );
737 }534 }
738535
739 #[test]536 #[test]
740 fn test_shorthand_empty_class_error() {537 fn test_shorthand_empty_class_error() {
741 // Empty class correctly errors538 assert!(parse_open("<div.>").is_err());
742 let result = parse_open("<div.>");
743 assert!(result.is_err());
744 }539 }
745540
746 #[test]541 #[test]
747 fn test_shorthand_empty_id_error() {542 fn test_shorthand_empty_id_error() {
748 // Empty id correctly errors543 assert!(parse_open("<div#>").is_err());
749 let result = parse_open("<div#>");
750 assert!(result.is_err());
751 }544 }
752545
753 // ===========================================546 // ===========================================
...@@ -756,475 +549,309 @@ mod tests {...@@ -756,475 +549,309 @@ mod tests {
756549
757 #[test]550 #[test]
758 fn test_tag_variable_simple() {551 fn test_tag_variable_simple() {
759 assert_eq!(552 check_open("<my-tag/foo>", "my-tag", "<my-tag/foo>", false);
760 parse_open("<my-tag/foo>"),
761 Ok(Open {
762 tag_name: "my-tag",
763 content: "<my-tag/foo>",
764 self_closing: false,
765 })
766 );
767 }553 }
768554
769 // BUG: Variable binding followed by /> fails
770 #[test]555 #[test]
771 fn test_tag_variable_self_closing() {556 fn test_tag_variable_self_closing() {
772 assert_eq!(557 check_open("<input/myInput/>", "input", "<input/myInput/>", true);
773 parse_open("<input/myInput/>"),
774 Ok(Open {
775 tag_name: "input",
776 content: "<input/myInput/>",
777 self_closing: true,
778 })
779 );
780 }558 }
781559
782 #[test]560 #[test]
783 fn test_tag_variable_destructure_object() {561 fn test_tag_variable_destructure_object() {
784 assert_eq!(562 check_open("<my-tag/{ a, b }>", "my-tag", "<my-tag/{ a, b }>", false);
785 parse_open("<my-tag/{ a, b }>"),
786 Ok(Open {
787 tag_name: "my-tag",
788 content: "<my-tag/{ a, b }>",
789 self_closing: false,
790 })
791 );
792 }563 }
793564
794 #[test]565 #[test]
795 fn test_tag_variable_destructure_array() {566 fn test_tag_variable_destructure_array() {
796 assert_eq!(567 check_open("<my-tag/[x, y]>", "my-tag", "<my-tag/[x, y]>", false);
797 parse_open("<my-tag/[x, y]>"),
798 Ok(Open {
799 tag_name: "my-tag",
800 content: "<my-tag/[x, y]>",
801 self_closing: false,
802 })
803 );
804 }568 }
805569
806 #[test]570 #[test]
807 fn test_tag_variable_with_type() {571 fn test_tag_variable_with_type() {
808 assert_eq!(572 check_open(
809 parse_open("<const/items: Item[]>"),573 "<const/items: Item[]>",
810 Ok(Open {574 "const",
811 tag_name: "const",575 "<const/items: Item[]>",
812 content: "<const/items: Item[]>",576 false,
813 self_closing: false,
814 })
815 );577 );
816 }578 }
817579
818 #[test]580 #[test]
819 fn test_tag_variable_with_attrs() {581 fn test_tag_variable_with_attrs() {
820 assert_eq!(582 check_open(
821 parse_open("<my-tag/result value=42>"),583 "<my-tag/result value=42>",
822 Ok(Open {584 "my-tag",
823 tag_name: "my-tag",585 "<my-tag/result value=42>",
824 content: "<my-tag/result value=42>",586 false,
825 self_closing: false,
826 })
827 );587 );
828 }588 }
829589
830 // ===========================================590 // ===========================================
831 // Tag arguments (args)591 // Tag arguments (args)
832 // BUG: Tag arguments without whitespace before ( fail
833 // The parser only enters argument parsing after skip_whitespace,
834 // but <tag()> has no whitespace before (
835 // ===========================================592 // ===========================================
836593
837 #[test]594 #[test]
838 fn test_tag_arguments_empty() {595 fn test_tag_arguments_empty() {
839 assert_eq!(596 check_open("<my-tag()>", "my-tag", "<my-tag()>", false);
840 parse_open("<my-tag()>"),
841 Ok(Open {
842 tag_name: "my-tag",
843 content: "<my-tag()>",
844 self_closing: false,
845 })
846 );
847 }597 }
848598
849 #[test]599 #[test]
850 fn test_tag_arguments_simple() {600 fn test_tag_arguments_simple() {
851 assert_eq!(601 check_open("<my-tag(1, 2, 3)>", "my-tag", "<my-tag(1, 2, 3)>", false);
852 parse_open("<my-tag(1, 2, 3)>"),
853 Ok(Open {
854 tag_name: "my-tag",
855 content: "<my-tag(1, 2, 3)>",
856 self_closing: false,
857 })
858 );
859 }602 }
860603
861 #[test]604 #[test]
862 fn test_tag_arguments_expressions() {605 fn test_tag_arguments_expressions() {
863 assert_eq!(606 check_open(
864 parse_open("<my-tag(a + b, fn())>"),607 "<my-tag(a + b, fn())>",
865 Ok(Open {608 "my-tag",
866 tag_name: "my-tag",609 "<my-tag(a + b, fn())>",
867 content: "<my-tag(a + b, fn())>",610 false,
868 self_closing: false,
869 })
870 );611 );
871 }612 }
872613
873 #[test]614 #[test]
874 fn test_tag_arguments_spread() {615 fn test_tag_arguments_spread() {
875 assert_eq!(616 check_open("<my-tag(...args)>", "my-tag", "<my-tag(...args)>", false);
876 parse_open("<my-tag(...args)>"),
877 Ok(Open {
878 tag_name: "my-tag",
879 content: "<my-tag(...args)>",
880 self_closing: false,
881 })
882 );
883 }617 }
884618
885 #[test]619 #[test]
886 fn test_tag_arguments_object() {620 fn test_tag_arguments_object() {
887 assert_eq!(621 check_open(
888 parse_open("<my-tag({ a: 1, b: 2 })>"),622 "<my-tag({ a: 1, b: 2 })>",
889 Ok(Open {623 "my-tag",
890 tag_name: "my-tag",624 "<my-tag({ a: 1, b: 2 })>",
891 content: "<my-tag({ a: 1, b: 2 })>",625 false,
892 self_closing: false,
893 })
894 );626 );
895 }627 }
896628
897 // Test that tag arguments WITH whitespace work
898 #[test]629 #[test]
899 fn test_tag_arguments_with_space() {630 fn test_tag_arguments_with_space() {
900 assert_eq!(631 check_open("<my-tag (1, 2, 3)>", "my-tag", "<my-tag (1, 2, 3)>", false);
901 parse_open("<my-tag (1, 2, 3)>"),
902 Ok(Open {
903 tag_name: "my-tag",
904 content: "<my-tag (1, 2, 3)>",
905 self_closing: false,
906 })
907 );
908 }632 }
909633
910 // ===========================================634 // ===========================================
911 // Attributes635 // Attributes
912 // ===========================================636 // ===========================================
913637
914 // BUG: String literals fail - expression parser doesn't terminate at > after string
915 #[test]638 #[test]
916 fn test_attr_simple_string() {639 fn test_attr_simple_string() {
917 assert_eq!(640 check_open("<div class=\"foo\">", "div", "<div class=\"foo\">", false);
918 parse_open("<div class=\"foo\">"),
919 Ok(Open {
920 tag_name: "div",
921 content: "<div class=\"foo\">",
922 self_closing: false,
923 })
924 );
925 }641 }
926642
927 #[test]643 #[test]
928 fn test_attr_expression() {644 fn test_attr_expression() {
929 // Identifier expressions work645 check_open("<div count=1 + 1>", "div", "<div count=1 + 1>", false);
930 assert_eq!(
931 parse_open("<div count=1 + 1>"),
932 Ok(Open {
933 tag_name: "div",
934 content: "<div count=1 + 1>",
935 self_closing: false,
936 })
937 );
938 }646 }
939647
940 // BUG: Template literals fail
941 #[test]648 #[test]
942 fn test_attr_template_literal() {649 fn test_attr_template_literal() {
943 assert_eq!(650 check_open(
944 parse_open("<div class=`hello ${name}`>"),651 "<div class=`hello ${name}`>",
945 Ok(Open {652 "div",
946 tag_name: "div",653 "<div class=`hello ${name}`>",
947 content: "<div class=`hello ${name}`>",654 false,
948 self_closing: false,
949 })
950 );655 );
951 }656 }
952657
953 #[test]658 #[test]
954 fn test_attr_spread() {659 fn test_attr_spread() {
955 assert_eq!(660 check_open("<div ...props>", "div", "<div ...props>", false);
956 parse_open("<div ...props>"),
957 Ok(Open {
958 tag_name: "div",
959 content: "<div ...props>",
960 self_closing: false,
961 })
962 );
963 }661 }
964662
965 // BUG: Object literals with > in them fail
966 #[test]663 #[test]
967 fn test_attr_spread_object() {664 fn test_attr_spread_object() {
968 assert_eq!(665 check_open(
969 parse_open("<div ...{ a: 1, b: 2 }>"),666 "<div ...{ a: 1, b: 2 }>",
970 Ok(Open {667 "div",
971 tag_name: "div",668 "<div ...{ a: 1, b: 2 }>",
972 content: "<div ...{ a: 1, b: 2 }>",669 false,
973 self_closing: false,
974 })
975 );670 );
976 }671 }
977672
978 // BUG: Multiple attributes with string values fail
979 #[test]673 #[test]
980 fn test_attr_multiple_spreads() {674 fn test_attr_multiple_spreads() {
981 assert_eq!(675 check_open(
982 parse_open("<div ...a ...b foo=\"bar\">"),676 "<div ...a ...b foo=\"bar\">",
983 Ok(Open {677 "div",
984 tag_name: "div",678 "<div ...a ...b foo=\"bar\">",
985 content: "<div ...a ...b foo=\"bar\">",679 false,
986 self_closing: false,
987 })
988 );680 );
989 }681 }
990682
991 #[test]683 #[test]
992 fn test_attr_two_way_binding() {684 fn test_attr_two_way_binding() {
993 assert_eq!(685 check_open(
994 parse_open("<counter value:=count>"),686 "<counter value:=count>",
995 Ok(Open {687 "counter",
996 tag_name: "counter",688 "<counter value:=count>",
997 content: "<counter value:=count>",689 false,
998 self_closing: false,
999 })
1000 );690 );
1001 }691 }
1002692
1003 #[test]693 #[test]
1004 fn test_attr_two_way_binding_property() {694 fn test_attr_two_way_binding_property() {
1005 assert_eq!(695 check_open(
1006 parse_open("<counter value:=input.count>"),696 "<counter value:=input.count>",
1007 Ok(Open {697 "counter",
1008 tag_name: "counter",698 "<counter value:=input.count>",
1009 content: "<counter value:=input.count>",699 false,
1010 self_closing: false,
1011 })
1012 );700 );
1013 }701 }
1014702
1015 #[test]703 #[test]
1016 fn test_attr_method_shorthand() {704 fn test_attr_method_shorthand() {
1017 assert_eq!(705 check_open(
1018 parse_open("<button onClick(e) { console.log(e) }>"),706 "<button onClick(e) { console.log(e) }>",
1019 Ok(Open {707 "button",
1020 tag_name: "button",708 "<button onClick(e) { console.log(e) }>",
1021 content: "<button onClick(e) { console.log(e) }>",709 false,
1022 self_closing: false,
1023 })
1024 );710 );
1025 }711 }
1026712
1027 #[test]713 #[test]
1028 fn test_attr_method_with_return_type() {714 fn test_attr_method_with_return_type() {
1029 assert_eq!(715 check_open(
1030 parse_open("<button onClick(e): void { console.log(e) }>"),716 "<button onClick(e): void { console.log(e) }>",
1031 Ok(Open {717 "button",
1032 tag_name: "button",718 "<button onClick(e): void { console.log(e) }>",
1033 content: "<button onClick(e): void { console.log(e) }>",719 false,
1034 self_closing: false,
1035 })
1036 );720 );
1037 }721 }
1038722
1039 #[test]723 #[test]
1040 fn test_attr_boolean() {724 fn test_attr_boolean() {
1041 assert_eq!(725 check_open(
1042 parse_open("<input type=\"checkbox\" checked>"),726 "<input type=\"checkbox\" checked>",
1043 Ok(Open {727 "input",
1044 tag_name: "input",728 "<input type=\"checkbox\" checked>",
1045 content: "<input type=\"checkbox\" checked>",729 false,
1046 self_closing: false,
1047 })
1048 );730 );
1049 }731 }
1050732
1051 #[test]733 #[test]
1052 fn test_attr_boolean_then_attr() {734 fn test_attr_boolean_then_attr() {
1053 assert_eq!(735 check_open(
1054 parse_open("<input checked foo=1>"),736 "<input checked foo=1>",
1055 Ok(Open {737 "input",
1056 tag_name: "input",738 "<input checked foo=1>",
1057 content: "<input checked foo=1>",739 false,
1058 self_closing: false,
1059 })
1060 );740 );
1061 }741 }
1062742
1063 #[test]743 #[test]
1064 fn test_attr_with_colon() {744 fn test_attr_with_colon() {
1065 assert_eq!(745 check_open(
1066 parse_open("<div data:value=\"test\">"),746 "<div data:value=\"test\">",
1067 Ok(Open {747 "div",
1068 tag_name: "div",748 "<div data:value=\"test\">",
1069 content: "<div data:value=\"test\">",749 false,
1070 self_closing: false,
1071 })
1072 );750 );
1073 }751 }
1074752
1075 #[test]753 #[test]
1076 fn test_shorthand_value() {754 fn test_shorthand_value() {
1077 assert_eq!(755 check_open("<my-tag=1>", "my-tag", "<my-tag=1>", false);
1078 parse_open("<my-tag=1>"),
1079 Ok(Open {
1080 tag_name: "my-tag",
1081 content: "<my-tag=1>",
1082 self_closing: false,
1083 })
1084 );
1085 }756 }
1086757
1087 #[test]758 #[test]
1088 fn test_shorthand_value_expression() {759 fn test_shorthand_value_expression() {
1089 assert_eq!(760 check_open("<my-tag=a + b>", "my-tag", "<my-tag=a + b>", false);
1090 parse_open("<my-tag=a + b>"),
1091 Ok(Open {
1092 tag_name: "my-tag",
1093 content: "<my-tag=a + b>",
1094 self_closing: false,
1095 })
1096 );
1097 }761 }
1098762
1099 #[test]763 #[test]
1100 fn test_shorthand_value_with_other_attrs() {764 fn test_shorthand_value_with_other_attrs() {
1101 assert_eq!(765 check_open(
1102 parse_open("<my-tag=42 foo=\"bar\">"),766 "<my-tag=42 foo=\"bar\">",
1103 Ok(Open {767 "my-tag",
1104 tag_name: "my-tag",768 "<my-tag=42 foo=\"bar\">",
1105 content: "<my-tag=42 foo=\"bar\">",769 false,
1106 self_closing: false,
1107 })
1108 );770 );
1109 }771 }
1110772
1111 #[test]773 #[test]
1112 fn test_value_method_shorthand() {774 fn test_value_method_shorthand() {
1113 assert_eq!(775 check_open(
1114 parse_open("<my-tag() { console.log(\"hi\") }>"),776 "<my-tag() { console.log(\"hi\") }>",
1115 Ok(Open {777 "my-tag",
1116 tag_name: "my-tag",778 "<my-tag() { console.log(\"hi\") }>",
1117 content: "<my-tag() { console.log(\"hi\") }>",779 false,
1118 self_closing: false,
1119 })
1120 );780 );
1121 }781 }
1122782
1123 #[test]783 #[test]
1124 fn test_value_method_shorthand_self_closing() {784 fn test_value_method_shorthand_self_closing() {
1125 assert_eq!(785 check_open(
1126 parse_open("<my-tag() { console.log(\"hi\") }/>"),786 "<my-tag() { console.log(\"hi\") }/>",
1127 Ok(Open {787 "my-tag",
1128 tag_name: "my-tag",788 "<my-tag() { console.log(\"hi\") }/>",
1129 content: "<my-tag() { console.log(\"hi\") }/>",789 true,
1130 self_closing: true,
1131 })
1132 );790 );
1133 }791 }
1134792
1135 #[test]793 #[test]
1136 fn test_value_method_shorthand_with_param() {794 fn test_value_method_shorthand_with_param() {
1137 assert_eq!(795 check_open(
1138 parse_open("<my-tag(e) { console.log(e) }>"),796 "<my-tag(e) { console.log(e) }>",
1139 Ok(Open {797 "my-tag",
1140 tag_name: "my-tag",798 "<my-tag(e) { console.log(e) }>",
1141 content: "<my-tag(e) { console.log(e) }>",799 false,
1142 self_closing: false,
1143 })
1144 );800 );
1145 }801 }
1146802
1147 #[test]803 #[test]
1148 fn test_combined_variable_and_args() {804 fn test_combined_variable_and_args() {
1149 assert_eq!(805 check_open("<for/item (items)>", "for", "<for/item (items)>", false);
1150 parse_open("<for/item (items)>"),
1151 Ok(Open {
1152 tag_name: "for",
1153 content: "<for/item (items)>",
1154 self_closing: false,
1155 })
1156 );
1157 }806 }
1158807
1159 #[test]808 #[test]
1160 fn test_combined_id_class_variable() {809 fn test_combined_id_class_variable() {
1161 assert_eq!(810 check_open("<div#myId.cls/ref>", "div", "<div#myId.cls/ref>", false);
1162 parse_open("<div#myId.cls/ref>"),
1163 Ok(Open {
1164 tag_name: "div",
1165 content: "<div#myId.cls/ref>",
1166 self_closing: false,
1167 })
1168 );
1169 }811 }
1170812
1171 #[test]813 #[test]
1172 fn test_combined_complex() {814 fn test_combined_complex() {
1173 assert_eq!(815 check_open(
1174 parse_open("<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>"),816 "<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>",
1175 Ok(Open {817 "my-tag",
1176 tag_name: "my-tag",818 "<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>",
1177 content: "<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>",819 false,
1178 self_closing: false,
1179 })
1180 );820 );
1181 }821 }
1182822
1183 #[test]823 #[test]
1184 fn test_attr_with_gt_in_parens() {824 fn test_attr_with_gt_in_parens() {
1185 assert_eq!(825 check_open("<div value=(a > b)>", "div", "<div value=(a > b)>", false);
1186 parse_open("<div value=(a > b)>"),
1187 Ok(Open {
1188 tag_name: "div",
1189 content: "<div value=(a > b)>",
1190 self_closing: false,
1191 })
1192 );
1193 }826 }
1194827
1195 #[test]828 #[test]
1196 fn test_attr_with_gt_in_brackets() {829 fn test_attr_with_gt_in_brackets() {
1197 assert_eq!(830 check_open(
1198 parse_open("<div value=arr[a > 0]>"),831 "<div value=arr[a > 0]>",
1199 Ok(Open {832 "div",
1200 tag_name: "div",833 "<div value=arr[a > 0]>",
1201 content: "<div value=arr[a > 0]>",834 false,
1202 self_closing: false,
1203 })
1204 );835 );
1205 }836 }
1206837
1207 #[test]838 #[test]
1208 fn test_attr_arrow_function() {839 fn test_attr_arrow_function() {
1209 assert_eq!(840 check_open(
1210 parse_open("<div onClick=() => console.log(1)>"),841 "<div onClick=() => console.log(1)>",
1211 Ok(Open {842 "div",
1212 tag_name: "div",843 "<div onClick=() => console.log(1)>",
1213 content: "<div onClick=() => console.log(1)>",844 false,
1214 self_closing: false,
1215 })
1216 );845 );
1217 }846 }
1218847
1219 #[test]848 #[test]
1220 fn test_attr_ternary() {849 fn test_attr_ternary() {
1221 assert_eq!(850 check_open(
1222 parse_open("<div class=isActive ? a : b> > > > >"),851 "<div class=isActive ? a : b> > > > >",
1223 Ok(Open {852 "div",
1224 tag_name: "div",853 "<div class=isActive ? a : b>",
1225 content: "<div class=isActive ? a : b>",854 false,
1226 self_closing: false,
1227 })
1228 );855 );
1229 }856 }
1230857
...@@ -1251,7 +878,7 @@ mod tests {...@@ -1251,7 +878,7 @@ mod tests {
1251 let result = parse_tag("<div>").unwrap();878 let result = parse_tag("<div>").unwrap();
1252 match result {879 match result {
1253 OpenOrClose::Open(open) => {880 OpenOrClose::Open(open) => {
1254 assert_eq!(open.tag_name, "div");881 assert_eq!(open.tag_name(), "div");
1255 }882 }
1256 OpenOrClose::Close(_) => panic!("Expected Open"),883 OpenOrClose::Close(_) => panic!("Expected Open"),
1257 }884 }
...@@ -1360,7 +987,7 @@ mod tests {...@@ -1360,7 +987,7 @@ mod tests {
1360 let result = parse_open("<div.color-${iconName}/>");987 let result = parse_open("<div.color-${iconName}/>");
1361 assert!(result.is_ok());988 assert!(result.is_ok());
1362 let open = result.unwrap();989 let open = result.unwrap();
1363 assert_eq!(open.content, "<div.color-${iconName}/>");990 assert_eq!(open.src, "<div.color-${iconName}/>");
1364 }991 }
1365992
1366 #[test]993 #[test]
...@@ -1368,7 +995,7 @@ mod tests {...@@ -1368,7 +995,7 @@ mod tests {
1368 let result = parse_open("<div#${myId}>");995 let result = parse_open("<div#${myId}>");
1369 assert!(result.is_ok());996 assert!(result.is_ok());
1370 let open = result.unwrap();997 let open = result.unwrap();
1371 assert_eq!(open.content, "<div#${myId}>");998 assert_eq!(open.src, "<div#${myId}>");
1372 }999 }
13731000
1374 #[test]1001 #[test]
...@@ -1376,7 +1003,7 @@ mod tests {...@@ -1376,7 +1003,7 @@ mod tests {
1376 let result = parse_open("<button.variant-${meow}#${id}>");1003 let result = parse_open("<button.variant-${meow}#${id}>");
1377 assert!(result.is_ok());1004 assert!(result.is_ok());
1378 let open = result.unwrap();1005 let open = result.unwrap();
1379 assert_eq!(open.content, "<button.variant-${meow}#${id}>");1006 assert_eq!(open.src, "<button.variant-${meow}#${id}>");
1380 }1007 }
13811008
1382 #[test]1009 #[test]
...@@ -1384,7 +1011,7 @@ mod tests {...@@ -1384,7 +1011,7 @@ mod tests {
1384 let result = parse_open("<div.class1.${dynamic}.class3>");1011 let result = parse_open("<div.class1.${dynamic}.class3>");
1385 assert!(result.is_ok());1012 assert!(result.is_ok());
1386 let open = result.unwrap();1013 let open = result.unwrap();
1387 assert_eq!(open.content, "<div.class1.${dynamic}.class3>");1014 assert_eq!(open.src, "<div.class1.${dynamic}.class3>");
1388 }1015 }
13891016
1390 #[test]1017 #[test]
...@@ -1392,7 +1019,7 @@ mod tests {...@@ -1392,7 +1019,7 @@ mod tests {
1392 let result = parse_open("<div.class-${a + b}>");1019 let result = parse_open("<div.class-${a + b}>");
1393 assert!(result.is_ok());1020 assert!(result.is_ok());
1394 let open = result.unwrap();1021 let open = result.unwrap();
1395 assert_eq!(open.content, "<div.class-${a + b}>");1022 assert_eq!(open.src, "<div.class-${a + b}>");
1396 }1023 }
13971024
1398 #[test]1025 #[test]
...@@ -1400,7 +1027,7 @@ mod tests {...@@ -1400,7 +1027,7 @@ mod tests {
1400 let result = parse_open("<div.class-$value>");1027 let result = parse_open("<div.class-$value>");
1401 assert!(result.is_ok());1028 assert!(result.is_ok());
1402 let open = result.unwrap();1029 let open = result.unwrap();
1403 assert_eq!(open.content, "<div.class-$value>");1030 assert_eq!(open.src, "<div.class-$value>");
1404 }1031 }
14051032
1406 #[test]1033 #[test]
...@@ -1408,4 +1035,82 @@ mod tests {...@@ -1408,4 +1035,82 @@ mod tests {
1408 let result = parse_open("<${MyComponent}/>");1035 let result = parse_open("<${MyComponent}/>");
1409 assert!(result.is_ok());1036 assert!(result.is_ok());
1410 }1037 }
1038
1039 #[test]
1040 fn test_id_attr_shorthand_static() {
1041 let open = parse_open("<div#myId>").unwrap();
1042 assert_eq!(open.id_attr(), Some(("myId", false)));
1043 assert!(!open.id_is_dynamic());
1044 }
1045
1046 #[test]
1047 fn test_id_attr_shorthand_with_hyphen() {
1048 let open = parse_open("<h2#technical-review>").unwrap();
1049 assert_eq!(open.id_attr(), Some(("technical-review", false)));
1050 }
1051
1052 #[test]
1053 fn test_id_attr_shorthand_dynamic() {
1054 let open = parse_open("<div#${myId}>").unwrap();
1055 assert_eq!(open.id_attr(), None);
1056 assert!(open.id_is_dynamic());
1057 }
1058
1059 #[test]
1060 fn test_id_attr_attribute_static() {
1061 let open = parse_open("<div id=\"my-id\">").unwrap();
1062 assert_eq!(open.id_attr(), Some(("\"my-id\"", true)));
1063 assert!(!open.id_is_dynamic());
1064 }
1065
1066 #[test]
1067 fn test_id_attr_attribute_single_quotes() {
1068 let open = parse_open("<div id='my-id'>").unwrap();
1069 assert_eq!(open.id_attr(), Some(("'my-id'", true)));
1070 }
1071
1072 #[test]
1073 fn test_id_attr_attribute_dynamic() {
1074 let open = parse_open("<div id=myVariable>").unwrap();
1075 assert_eq!(open.id_attr(), None);
1076 assert!(open.id_is_dynamic());
1077 }
1078
1079 #[test]
1080 fn test_id_attr_none() {
1081 let open = parse_open("<div class=\"foo\">").unwrap();
1082 assert_eq!(open.id_attr(), None);
1083 assert!(!open.id_is_dynamic());
1084 }
1085
1086 #[test]
1087 fn test_id_attr_shorthand_with_classes() {
1088 let open = parse_open("<div.cls1#myId.cls2>").unwrap();
1089 assert_eq!(open.id_attr(), Some(("myId", false)));
1090 }
1091
1092 #[test]
1093 fn test_set_id_attr_insert_new() {
1094 let open = parse_open("<div>").unwrap();
1095 let mut owned = open.to_owned();
1096 owned.insert_id_attr("new-id");
1097 assert_eq!(owned.src, "<div#new-id>");
1098 assert_eq!(owned.as_ref().id_attr(), Some(("new-id", false)));
1099 }
1100
1101 #[test]
1102 fn test_set_id_attr_insert_with_attrs() {
1103 let open = parse_open("<div class=\"foo\">").unwrap();
1104 let mut owned = open.to_owned();
1105 owned.insert_id_attr("new-id");
1106 assert_eq!(owned.src, "<div#new-id class=\"foo\">");
1107 }
1108
1109 #[test]
1110 fn test_set_id_attr_self_closing() {
1111 let open = parse_open("<input/>").unwrap();
1112 let mut owned = open.to_owned();
1113 owned.insert_id_attr("my-input");
1114 assert_eq!(owned.src, "<input#my-input/>");
1115 }
1411}1116}
src/marko_ast.rs created+230
...@@ -0,0 +1,230 @@
1use oxc_diagnostics::OxcDiagnostic;
2use oxc_span::Span;
3
4use crate::{err, typescript::parse_expr};
5
6/// Information about the id attribute in a tag.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum AttributeValue {
9 /// No id attribute found
10 None,
11 /// Staticly analyzed string literal (raw source)
12 Static { span: Span, is_quoted: bool },
13 /// Dynamic expression
14 Dynamic,
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct Open<'a> {
19 pub src: &'a str,
20 /// Only set if the tag name is a static value
21 literal_tag_name: Span,
22 pub self_closing: bool,
23 /// Offset after tag name and class/id shorthands, where new shorthand could be inserted
24 shorthand_end: u32,
25 /// Information about the id attribute
26 pub id: AttributeValue,
27}
28
29impl<'a> Open<'a> {
30 /// Convert to an owned version for storage.
31 pub fn to_owned(&self) -> OpenOwned {
32 OpenOwned {
33 src: self.src.to_string(),
34 literal_tag_name: self.literal_tag_name,
35 self_closing: self.self_closing,
36 shorthand_end: self.shorthand_end,
37 id: self.id,
38 }
39 }
40
41 /// Returns the element tag name. Empty string when dynamic interpolation.
42 pub fn tag_name(&self) -> &'a str {
43 &self.src[self.literal_tag_name]
44 }
45
46 /// Returns the span of the tag's name for errors. Works for dynamic interpolations
47 pub fn tag_name_span(&self) -> Span {
48 // fast case, when a literal tag name is passed
49 if !self.literal_tag_name.is_empty() {
50 self.literal_tag_name.expand_left(1)
51 } else {
52 // slow path, re-parse the js expression
53 let mut l = LexState::new(self.src);
54 l.advance(3);
55 parse_expr(&mut l).expect("validated text should pass");
56 Span::new(3, l.offset() as u32)
57 }
58 }
59
60 /// Returns the id attribute value if it's a static string.
61 /// Returns `None` if no id or if the id is dynamic.
62 pub fn id_attr(&self) -> Option<(&'a str, bool)> {
63 match self.id {
64 AttributeValue::Static { span, is_quoted } => Some((&self.src[span], is_quoted)),
65 AttributeValue::None | AttributeValue::Dynamic => None,
66 }
67 }
68
69 /// Returns whether the id is dynamic (contains ${} or is a non-string expression).
70 pub fn id_is_dynamic(&self) -> bool {
71 matches!(self.id, AttributeValue::Dynamic)
72 }
73
74 pub(crate) fn new(
75 src: &'a str,
76 tag_name: Span,
77 self_closing: bool,
78 shorthand_end: u32,
79 id: AttributeValue,
80 ) -> Self {
81 Self {
82 src,
83 literal_tag_name: tag_name,
84 self_closing,
85 shorthand_end,
86 id,
87 }
88 }
89}
90
91/// Owned version of [Open] for storage in AST nodes, which do not allow specifying a lifetime.
92/// Waste of memory.
93#[derive(Debug, Clone, PartialEq)]
94pub struct OpenOwned {
95 pub src: String,
96 literal_tag_name: Span,
97 pub self_closing: bool,
98 /// Offset after tag name and class/id shorthands, where new shorthand could be inserted
99 shorthand_end: u32,
100 /// Information about the id attribute
101 pub id: AttributeValue,
102}
103
104impl OpenOwned {
105 /// Borrow as an [Open] reference to access methods.
106 pub fn as_ref(&self) -> Open<'_> {
107 Open {
108 src: &self.src,
109 literal_tag_name: self.literal_tag_name,
110 self_closing: self.self_closing,
111 shorthand_end: self.shorthand_end,
112 id: self.id,
113 }
114 }
115
116 /// Set the id attribute to a static string value.
117 /// If an id already exists (shorthand or attribute), it is replaced.
118 /// If no id exists, a shorthand #id is inserted after the tag name/classes.
119 pub fn insert_id_attr(&mut self, new_id: &str) {
120 match self.id {
121 AttributeValue::None => {
122 // Insert new shorthand #id at shorthand_end position
123 let insert_pos = self.shorthand_end;
124 let mut src = String::with_capacity(self.src.len() + 1 + new_id.len());
125 src.push_str(&self.src[..insert_pos as usize]);
126 src.push('#');
127 src.push_str(new_id);
128 src.push_str(&self.src[insert_pos as usize..]);
129
130 let id_start = insert_pos + 1; // after the #
131 let id_end = id_start + new_id.len() as u32;
132
133 self.src = src;
134 self.shorthand_end = id_end;
135 self.id = AttributeValue::Static {
136 span: Span::new(id_start, id_end),
137 is_quoted: false,
138 };
139 }
140 AttributeValue::Dynamic | AttributeValue::Static { .. } => {
141 panic!("Cannot replace ID attribute");
142 }
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct Close<'a> {
149 pub tag_name: Option<&'a str>,
150 pub length: u32,
151}
152
153impl Close<'_> {
154 /// Returns the span of the tag name relative to the start of the tag.
155 /// For `</div>`, returns Span(2, 5) pointing to "div".
156 /// For `</>`, returns None.
157 pub fn tag_name_span(&self) -> Option<Span> {
158 self.tag_name.map(|name| {
159 // Close tag starts with '</', so tag name starts at offset 2
160 let start = 2u32;
161 let end = start + (name.len() as u32);
162 Span::new(start, end)
163 })
164 }
165}
166
167pub enum OpenOrClose<'a> {
168 Open(Open<'a>),
169 Close(Close<'a>),
170}
171
172pub struct LexState<'a> {
173 pub src: &'a str,
174 pub offset: u32,
175}
176
177impl<'a> LexState<'a> {
178 pub fn new(src: &'a str) -> LexState<'a> {
179 LexState { src, offset: 0 }
180 }
181
182 pub fn offset(&self) -> u32 {
183 self.offset
184 }
185
186 pub fn advance(&mut self, n: u32) {
187 self.offset += n;
188 }
189
190 pub fn restore(&mut self, n: u32) {
191 self.offset = n;
192 }
193
194 pub fn peek_byte(&self) -> Option<u8> {
195 self.src.as_bytes().get(self.offset as usize).copied()
196 }
197
198 pub fn peek_rest(&self) -> &'a str {
199 &self.src[self.offset as usize..]
200 }
201
202 pub fn expect_byte(&mut self) -> Result<u8, OxcDiagnostic> {
203 if let Some(b) = self.peek_byte() {
204 self.offset += 1;
205 return Ok(b);
206 }
207 Err(err("Unexpected end of file", self.offset, 1))
208 }
209
210 pub fn expect(&mut self, expected: &str) -> Result<(), OxcDiagnostic> {
211 if self.src[self.offset as usize..].starts_with(expected) {
212 self.offset += expected.len() as u32;
213 Ok(())
214 } else {
215 Err(err(format!("Expected {expected}"), self.offset, 1))
216 }
217 }
218
219 pub fn skip_whitespace(&mut self) {
220 loop {
221 match self.peek_byte() {
222 Some(byte) => match byte {
223 b' ' | b'\n' | b'\t' | b'\r' => self.offset += 1,
224 _ => break,
225 },
226 None => break,
227 }
228 }
229 }
230}
src/plugin/mod.rs+1-1
...@@ -35,7 +35,7 @@ pub(crate) struct ErrorBlock {...@@ -35,7 +35,7 @@ pub(crate) struct ErrorBlock {
35}35}
3636
37impl NodeValue for ErrorBlock {37impl NodeValue for ErrorBlock {
38 fn render(&self, _: &Node, _: &mut dyn Renderer) {38 fn render(&self, _: &Node, r: &mut dyn Renderer) {
39 panic!("cannot render ErrorBlock");39 panic!("cannot render ErrorBlock");
40 }40 }
41}41}
src/plugin/statement.rs+8-5
...@@ -36,17 +36,17 @@ impl BlockRule for Rule {...@@ -36,17 +36,17 @@ impl BlockRule for Rule {
36 .iter()36 .iter()
37 .find(|k| **k == keyword)37 .find(|k| **k == keyword)
38 .map(|k| k.len())38 .map(|k| k.len())
39 .unwrap_or(0);39 .unwrap_or(0) as u32;
4040
41 let unbounded_src = &state.src[state.line_offsets[state.line].first_nonspace..];41 let unbounded_src = &state.src[state.line_offsets[state.line].first_nonspace..];
4242
43 let statement_end =43 let statement_end =
44 match scan_first_statement_forbid_trailing(&unbounded_src[keyword_trim..]) {44 match scan_first_statement_forbid_trailing(&unbounded_src[keyword_trim as usize..]) {
45 Ok(ok) => ok,45 Ok(ok) => ok,
46 Err(err) => {46 Err(err) => {
47 return Some((47 return Some((
48 Node::new(ErrorBlock {48 Node::new(ErrorBlock {
49 errors: vec![adjust_err(err, keyword_trim.cast_signed())],49 errors: vec![adjust_err(err, keyword_trim as isize)],
50 }),50 }),
51 1,51 1,
52 ));52 ));
...@@ -54,8 +54,11 @@ impl BlockRule for Rule {...@@ -54,8 +54,11 @@ impl BlockRule for Rule {
54 };54 };
5555
56 let total_end = keyword_trim + statement_end;56 let total_end = keyword_trim + statement_end;
57 let content = &unbounded_src[0..unbounded_src.len().min(total_end + 1)];57 let content = &unbounded_src[0..unbounded_src.len().min(total_end as usize + 1)];
58 let line_count = 1 + content[..total_end].chars().filter(|&c| c == '\n').count();58 let line_count = 1 + content[..total_end as usize]
59 .chars()
60 .filter(|&c| c == '\n')
61 .count();
5962
60 let node = Node::new(StatementBlock {63 let node = Node::new(StatementBlock {
61 content: content.into(),64 content: content.into(),
src/plugin/tags.rs+43-66
...@@ -2,21 +2,19 @@ use markdown_it::parser::block::{BlockRule, BlockState};...@@ -2,21 +2,19 @@ use markdown_it::parser::block::{BlockRule, BlockState};
2use markdown_it::parser::inline::{InlineRoot, InlineRule, InlineState};2use markdown_it::parser::inline::{InlineRoot, InlineRule, InlineState};
3use markdown_it::{Node, NodeValue, Renderer};3use markdown_it::{Node, NodeValue, Renderer};
44
5use crate::marko::{self, OpenOrClose};5use crate::marko_ast::OpenOrClose;
6use crate::plugin::{get_line_raw, ErrorBlock};6use crate::plugin::{get_line_raw, ErrorBlock};
7use crate::{marko, marko_ast};
78
8/// An opening Marko tag: <div>, <if=cond>, <for|item| of=items>, etc.9/// An opening Marko tag: <div>, <if=cond>, <for|item| of=items>, etc.
9#[derive(Debug)]10#[derive(Debug)]
10pub struct MarkoOpen {11pub struct MarkoOpen {
11 pub content: String,12 pub open: marko_ast::OpenOwned,
12 pub tag_name: String,
13 /// Length of the tag name (for error span highlighting)
14 pub tag_name_len: usize,
15}13}
1614
17impl NodeValue for MarkoOpen {15impl NodeValue for MarkoOpen {
18 fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {16 fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {
19 fmt.text_raw(&self.content);17 fmt.text_raw(&self.open.src);
20 fmt.text_raw("\n");18 fmt.text_raw("\n");
21 }19 }
22}20}
...@@ -54,15 +52,12 @@ impl NodeValue for MarkoSelfClosing {...@@ -54,15 +52,12 @@ impl NodeValue for MarkoSelfClosing {
54/// The text is parsed as inline markdown (children) but not wrapped in <p>52/// The text is parsed as inline markdown (children) but not wrapped in <p>
55#[derive(Debug)]53#[derive(Debug)]
56pub struct MarkoOpenWithText {54pub struct MarkoOpenWithText {
57 pub open_tag: String,55 pub open: marko_ast::OpenOwned,
58 pub tag_name: String,
59 /// Length of the tag name (for error span highlighting)
60 pub tag_name_len: usize,
61}56}
6257
63impl NodeValue for MarkoOpenWithText {58impl NodeValue for MarkoOpenWithText {
64 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {59 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
65 fmt.text_raw(&self.open_tag);60 fmt.text_raw(&self.open.src);
66 fmt.text_raw("\n");61 fmt.text_raw("\n");
67 fmt.contents(&node.children);62 fmt.contents(&node.children);
68 fmt.text_raw("\n");63 fmt.text_raw("\n");
...@@ -157,37 +152,31 @@ fn find_same_line_close<'a>(content: &'a str, tag_name: &str) -> Option<(&'a str...@@ -157,37 +152,31 @@ fn find_same_line_close<'a>(content: &'a str, tag_name: &str) -> Option<(&'a str
157152
158 // Try to parse as a tag153 // Try to parse as a tag
159 match marko::parse_tag(&content[lt_pos..]) {154 match marko::parse_tag(&content[lt_pos..]) {
160 Ok(marko::OpenOrClose::Open(open)) => {155 Ok(OpenOrClose::Open(open)) => {
161 if !open.self_closing {156 if !open.self_closing {
162 stack.push(open.tag_name);157 stack.push(open.tag_name());
163 }158 }
164 pos = lt_pos + open.content.len();159 pos = lt_pos + (open.src.len());
165 }160 }
166 Ok(marko::OpenOrClose::Close(close)) => {161 Ok(OpenOrClose::Close(close)) => {
167 if stack.is_empty() {162 if stack.is_empty() {
168 // No nested tags open - this close tag is for the outer tag
169 // Check if it matches our tag name or is generic </>
170 if close.tag_name.map(|n| n == tag_name).unwrap_or(true) {163 if close.tag_name.map(|n| n == tag_name).unwrap_or(true) {
171 let content_before = &content[..lt_pos];164 let content_before = &content[..lt_pos];
172 let close_tag = &content[lt_pos..lt_pos + close.length];165 let close_tag = &content[lt_pos..lt_pos + close.length as usize];
173 return Some((content_before, close_tag));166 return Some((content_before, close_tag));
174 }167 }
175 // Named close tag that doesn't match - keep scanning168 pos = lt_pos + close.length as usize;
176 pos = lt_pos + close.length;
177 } else {169 } else {
178 // Close tag for a nested open tag - pop the stack
179 // Generic </> closes the most recent, named must match
180 if close.tag_name.is_none() {170 if close.tag_name.is_none() {
181 stack.pop();171 stack.pop();
182 } else if stack.last() == close.tag_name.as_ref() {172 } else if stack.last() == close.tag_name.as_ref() {
183 stack.pop();173 stack.pop();
184 }174 }
185 // If it doesn't match, we still continue (malformed nesting)175 pos = lt_pos + close.length as usize;
186 pos = lt_pos + close.length;
187 }176 }
188 }177 }
189 Err(_) => {178 Err(_) => {
190 // Not a valid tag, skip past this '<'179 // TODO: why no forward
191 pos = lt_pos + 1;180 pos = lt_pos + 1;
192 }181 }
193 }182 }
...@@ -212,13 +201,13 @@ impl BlockRule for Rule {...@@ -212,13 +201,13 @@ impl BlockRule for Rule {
212 match marko::parse_tag(unbounded_src) {201 match marko::parse_tag(unbounded_src) {
213 Ok(OpenOrClose::Open(open)) => {202 Ok(OpenOrClose::Open(open)) => {
214 // Count how many lines this tag spans203 // Count how many lines this tag spans
215 let lines_consumed = 1 + open.content.chars().filter(|&c| c == '\n').count();204 let lines_consumed = 1 + open.src.chars().filter(|&c| c == '\n').count();
216205
217 if open.self_closing {206 if open.self_closing {
218 // <input /> - self-closing tag207 // <input /> - self-closing tag
219 return Some((208 return Some((
220 Node::new(MarkoSelfClosing {209 Node::new(MarkoSelfClosing {
221 content: open.content.to_string(),210 content: open.src.to_string(),
222 }),211 }),
223 lines_consumed,212 lines_consumed,
224 ));213 ));
...@@ -226,18 +215,19 @@ impl BlockRule for Rule {...@@ -226,18 +215,19 @@ impl BlockRule for Rule {
226215
227 // Check if there's content on the same line after the tag216 // Check if there's content on the same line after the tag
228 // We need to look at unbounded_src, not trimmed, since tag may span multiple lines217 // We need to look at unbounded_src, not trimmed, since tag may span multiple lines
229 let rest_after_tag = &unbounded_src[open.content.len()..];218 let rest_after_tag = &unbounded_src[open.src.len()..];
230 let rest_of_last_line = rest_after_tag.lines().next().unwrap_or("");219 let rest_of_last_line = rest_after_tag.lines().next().unwrap_or("");
231 if !rest_of_last_line.trim().is_empty() {220 if !rest_of_last_line.trim().is_empty() {
232 let text = rest_of_last_line.trim();221 let text = rest_of_last_line.trim();
233222
234 // Check if the same-line content contains a matching close tag223 // Check if the same-line content contains a matching close tag
235 if let Some((content, close_tag)) = find_same_line_close(text, open.tag_name) {224 if let Some((content, close_tag)) = find_same_line_close(text, open.tag_name())
225 {
236 // Complete tag on one line: <tag>content</tag>226 // Complete tag on one line: <tag>content</tag>
237 let content = content.trim();227 let content = content.trim();
238228
239 // Calculate byte offset for source mapping229 // Calculate byte offset for source mapping
240 let text_start_in_line = open.content.len()230 let text_start_in_line = open.src.len()
241 + (rest_of_last_line.len() - rest_of_last_line.trim_start().len());231 + (rest_of_last_line.len() - rest_of_last_line.trim_start().len());
242 let line_start = state.line_offsets[state.line].first_nonspace;232 let line_start = state.line_offsets[state.line].first_nonspace;
243 let text_start = line_start + text_start_in_line;233 let text_start = line_start + text_start_in_line;
...@@ -245,10 +235,10 @@ impl BlockRule for Rule {...@@ -245,10 +235,10 @@ impl BlockRule for Rule {
245 let mapping = vec![(0, text_start)];235 let mapping = vec![(0, text_start)];
246236
247 let mut node = Node::new(MarkoBlockComplete {237 let mut node = Node::new(MarkoBlockComplete {
248 open_tag: open.content.to_string(),238 open_tag: open.src.to_string(),
249 close_tag: close_tag.to_string(),239 close_tag: close_tag.to_string(),
250 tag_name: open.tag_name.to_string(),240 tag_name: open.tag_name().to_string(),
251 tag_name_len: open.tag_name.len(),241 tag_name_len: open.tag_name().len(),
252 });242 });
253243
254 if !content.is_empty() {244 if !content.is_empty() {
...@@ -261,7 +251,7 @@ impl BlockRule for Rule {...@@ -261,7 +251,7 @@ impl BlockRule for Rule {
261251
262 // No close tag - just content after open tag252 // No close tag - just content after open tag
263 // Parse the text as inline markdown (not wrapped in paragraph)253 // Parse the text as inline markdown (not wrapped in paragraph)
264 let text_start_in_line = open.content.len()254 let text_start_in_line = open.src.len()
265 + (rest_of_last_line.len() - rest_of_last_line.trim_start().len());255 + (rest_of_last_line.len() - rest_of_last_line.trim_start().len());
266 let line_start = state.line_offsets[state.line].first_nonspace;256 let line_start = state.line_offsets[state.line].first_nonspace;
267 let text_start = line_start + text_start_in_line;257 let text_start = line_start + text_start_in_line;
...@@ -269,9 +259,7 @@ impl BlockRule for Rule {...@@ -269,9 +259,7 @@ impl BlockRule for Rule {
269 let mapping = vec![(0, text_start)];259 let mapping = vec![(0, text_start)];
270260
271 let mut node = Node::new(MarkoOpenWithText {261 let mut node = Node::new(MarkoOpenWithText {
272 open_tag: open.content.to_string(),262 open: open.to_owned(),
273 tag_name: open.tag_name.to_string(),
274 tag_name_len: open.tag_name.len(),
275 });263 });
276 node.children264 node.children
277 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));265 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));
...@@ -295,9 +283,7 @@ impl BlockRule for Rule {...@@ -295,9 +283,7 @@ impl BlockRule for Rule {
295 let mapping = vec![(0, next_line_start)];283 let mapping = vec![(0, next_line_start)];
296284
297 let mut node = Node::new(MarkoOpenWithText {285 let mut node = Node::new(MarkoOpenWithText {
298 open_tag: open.content.to_string(),286 open: open.to_owned(),
299 tag_name: open.tag_name.to_string(),
300 tag_name_len: open.tag_name.len(),
301 });287 });
302 node.children288 node.children
303 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));289 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));
...@@ -310,9 +296,7 @@ impl BlockRule for Rule {...@@ -310,9 +296,7 @@ impl BlockRule for Rule {
310 // Clean case: open tag on its own line(s), followed by blank or another tag296 // Clean case: open tag on its own line(s), followed by blank or another tag
311 Some((297 Some((
312 Node::new(MarkoOpen {298 Node::new(MarkoOpen {
313 content: open.content.to_string(),299 open: open.to_owned(),
314 tag_name: open.tag_name.to_string(),
315 tag_name_len: open.tag_name.len(),
316 }),300 }),
317 lines_consumed,301 lines_consumed,
318 ))302 ))
...@@ -320,15 +304,15 @@ impl BlockRule for Rule {...@@ -320,15 +304,15 @@ impl BlockRule for Rule {
320 Ok(OpenOrClose::Close(close)) => {304 Ok(OpenOrClose::Close(close)) => {
321 // Close tags should be single line, but check anyway305 // Close tags should be single line, but check anyway
322 let lines_consumed = 1 + close.length.saturating_sub(1).min(306 let lines_consumed = 1 + close.length.saturating_sub(1).min(
323 unbounded_src[..close.length]307 unbounded_src[..close.length as usize]
324 .chars()308 .chars()
325 .filter(|&c| c == '\n')309 .filter(|&c| c == '\n')
326 .count(),310 .count() as u32,
327 );311 );
328312
329 // Check if there's content on the same line after the close tag313 // Check if there's content on the same line after the close tag
330 let close_text = &unbounded_src[..close.length];314 let close_text = &unbounded_src[..close.length as usize];
331 let rest_after_tag = &unbounded_src[close.length..];315 let rest_after_tag = &unbounded_src[close.length as usize..];
332 let rest_of_line = rest_after_tag.lines().next().unwrap_or("");316 let rest_of_line = rest_after_tag.lines().next().unwrap_or("");
333 if !rest_of_line.trim().is_empty() {317 if !rest_of_line.trim().is_empty() {
334 // TODO: content after close tag on same line318 // TODO: content after close tag on same line
...@@ -341,7 +325,7 @@ impl BlockRule for Rule {...@@ -341,7 +325,7 @@ impl BlockRule for Rule {
341 tag_name: close.tag_name.map(|s| s.to_string()),325 tag_name: close.tag_name.map(|s| s.to_string()),
342 tag_name_len: close.tag_name.map(|s| s.len()),326 tag_name_len: close.tag_name.map(|s| s.len()),
343 }),327 }),
344 lines_consumed,328 lines_consumed as usize,
345 ))329 ))
346 }330 }
347 Err(err) => Some((Node::new(ErrorBlock { errors: vec![err] }), 1)),331 Err(err) => Some((Node::new(ErrorBlock { errors: vec![err] }), 1)),
...@@ -354,50 +338,43 @@ impl InlineRule for Rule {...@@ -354,50 +338,43 @@ impl InlineRule for Rule {
354338
355 fn run(state: &mut InlineState) -> Option<(Node, usize)> {339 fn run(state: &mut InlineState) -> Option<(Node, usize)> {
356 let input = &state.src[state.pos..state.pos_max];340 let input = &state.src[state.pos..state.pos_max];
357
358 // Skip if this doesn't look like a Marko tag
359 if !input.starts_with("<") {341 if !input.starts_with("<") {
360 return None;342 return None;
361 }343 }
362344
363 // Try to parse as a Marko tag
364 match marko::parse_tag(input) {345 match marko::parse_tag(input) {
365 Ok(OpenOrClose::Open(open)) => {346 Ok(OpenOrClose::Open(open)) => {
366 if open.self_closing {347 if open.self_closing {
367 // Self-closing inline tag like <br/>
368 return Some((348 return Some((
369 Node::new(MarkoSelfClosing {349 Node::new(MarkoSelfClosing {
370 content: open.content.to_string(),350 content: open.src.to_string(),
371 }),351 }),
372 open.content.len(),352 open.src.len(),
373 ));353 ));
374 }354 }
375 // Emit open marker - will be matched by post-processor355
376 Some((356 Some((
377 Node::new(MarkoInlineOpenMarker {357 Node::new(MarkoInlineOpenMarker {
378 content: open.content.to_string(),358 content: open.src.to_string(),
379 tag_name: open.tag_name.to_string(),359 tag_name: open.tag_name().to_owned(),
380 tag_name_len: open.tag_name.len(),360 tag_name_len: open.tag_name().len(),
381 }),361 }),
382 open.content.len(),362 open.src.len(),
383 ))363 ))
384 }364 }
385 Ok(OpenOrClose::Close(close)) => {365 Ok(OpenOrClose::Close(close)) => {
386 // Emit close marker - will be matched by post-processor366 let close_text = &input[..close.length as usize];
387 let close_text = &input[..close.length];
388 Some((367 Some((
389 Node::new(MarkoInlineCloseMarker {368 Node::new(MarkoInlineCloseMarker {
390 content: close_text.to_string(),369 content: close_text.to_string(),
391 tag_name: close.tag_name.map(|s| s.to_string()),370 tag_name: close.tag_name.map(|s| s.to_string()),
392 tag_name_len: close.tag_name.map(|s| s.len()),371 tag_name_len: close.tag_name.map(|s| s.len()),
393 }),372 }),
394 close.length,373 close.length as usize,
395 ))374 ))
396 }375 }
397 Err(_) => {376 // TODO: why this no reported
398 // Not a valid Marko tag, let other rules handle it377 Err(_) => None,
399 None
400 }
401 }378 }
402 }379 }
403}380}
src/plugin/toc.rs+3-3
...@@ -84,14 +84,14 @@ fn collect_headings_recursive(...@@ -84,14 +84,14 @@ fn collect_headings_recursive(
84 }84 }
85 // Check for MarkoOpen tags that are h1-h685 // Check for MarkoOpen tags that are h1-h6
86 else if let Some(open) = node.cast::<super::tags::MarkoOpen>() {86 else if let Some(open) = node.cast::<super::tags::MarkoOpen>() {
87 if let Some(level) = parse_heading_level(&open.tag_name) {87 if let Some(level) = parse_heading_level(&open.open.as_ref().tag_name()) {
88 let text = node88 let text = node
89 .children89 .children
90 .iter()90 .iter()
91 .map(|c| c.collect_text())91 .map(|c| c.collect_text())
92 .collect::<Vec<_>>()92 .collect::<Vec<_>>()
93 .join("");93 .join("");
94 let id = if let Some(existing) = open.content.strip_prefix('<').and_then(|s| {94 let id = if let Some(existing) = open.open.src.strip_prefix('<').and_then(|s| {
95 s.find("id=\"")95 s.find("id=\"")
96 .map(|pos| {96 .map(|pos| {
97 let start = pos + 4;97 let start = pos + 4;
...@@ -113,7 +113,7 @@ fn collect_headings_recursive(...@@ -113,7 +113,7 @@ fn collect_headings_recursive(
113 }113 }
114 // Check for MarkoOpenWithText tags that are h1-h6114 // Check for MarkoOpenWithText tags that are h1-h6
115 else if let Some(open) = node.cast::<super::tags::MarkoOpenWithText>() {115 else if let Some(open) = node.cast::<super::tags::MarkoOpenWithText>() {
116 if let Some(level) = parse_heading_level(open.tag_name.as_str()) {116 if let Some(level) = parse_heading_level(open.open.as_ref().tag_name()) {
117 let text = node117 let text = node
118 .children118 .children
119 .iter()119 .iter()
src/typescript.rs+66-70
...@@ -1,23 +1,22 @@...@@ -1,23 +1,22 @@
1//! implement ts partial parsing helpers1//! implements ts partial parsing helpers
2use crate::{adjust_err, err, marko::LexState};2use crate::{adjust_err, err, marko_ast::LexState};
33
4use oxc_allocator::Allocator;4use oxc_allocator::Allocator;
5use oxc_ast::ast::{Expression, Statement};5use oxc_ast::ast::{Expression, Statement};
6use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};6use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
7use oxc_span::{GetSpan, SourceType};7use oxc_span::{GetSpan, SourceType};
88
9/// TODO: Replace with oxc parser, but oxc is tough here9pub fn scan_first_statement_forbid_trailing(source: &str) -> Result<u32, OxcDiagnostic> {
10pub fn scan_first_statement_forbid_trailing(source: &str) -> Result<usize, OxcDiagnostic> {
11 let mut allocator = Allocator::new();10 let mut allocator = Allocator::new();
12 let expr = parse_stmt_extra(source, 0, &mut allocator)?;11 let expr = parse_stmt_extra(source, 0, &mut allocator)?;
13 let span = expr.span();12 let span = expr.span();
14 let len = (span.end - span.start) as usize;13 let len = span.end - span.start;
1514
16 if let Some(trailing) = source[span.end as usize..].lines().next() {15 if let Some(trailing) = source[span.end as usize..].lines().next() {
17 if trailing.trim().len() > 0 {16 if trailing.trim().len() > 0 {
18 return Err(err(17 return Err(err(
19 "Trailing content not allowed here",18 "Trailing content not allowed here",
20 span.end as usize + (trailing.len() - trailing.trim_start().len()),19 span.end + (trailing.len() - trailing.trim_start().len()) as u32,
21 trailing.trim().len(),20 trailing.trim().len(),
22 ));21 ));
23 }22 }
...@@ -33,7 +32,7 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(...@@ -33,7 +32,7 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
33 if source.is_empty() {32 if source.is_empty() {
34 return Err(err(33 return Err(err(
35 "Expected expression, found end of file",34 "Expected expression, found end of file",
36 offset.max(0).cast_unsigned(),35 offset.max(0).cast_unsigned() as u32,
37 1,36 1,
38 ));37 ));
39 }38 }
...@@ -52,7 +51,13 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(...@@ -52,7 +51,13 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
52 let first_err_offset = first_err51 let first_err_offset = first_err
53 .labels52 .labels
54 .as_ref()53 .as_ref()
55 .ok_or_else(|| err("Expected statement", offset.max(0).cast_unsigned(), 1))?54 .ok_or_else(|| {
55 err(
56 "Expected statement",
57 offset.max(0).cast_unsigned() as u32,
58 1,
59 )
60 })?
56 .first()61 .first()
57 .expect("labels, but no labels!")62 .expect("labels, but no labels!")
58 .offset();63 .offset();
...@@ -82,17 +87,18 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(...@@ -82,17 +87,18 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
82 assert!(!result.panicked);87 assert!(!result.panicked);
83 }88 }
8489
85 result90 result.program.body.into_iter().next().ok_or_else(|| {
86 .program91 err(
87 .body92 "Expected statement",
88 .into_iter()93 offset.max(0).cast_unsigned() as u32,
89 .next()94 1,
90 .ok_or_else(|| err("Expected statement", offset.max(0).cast_unsigned(), 1))95 )
96 })
91}97}
9298
93fn parse_expr_extra<'alloc, 'src: 'alloc>(99fn parse_expr_extra<'alloc, 'src: 'alloc>(
94 source: &'src str,100 source: &'src str,
95 offset: isize,101 offset: i32,
96 allocator: &'alloc mut Allocator,102 allocator: &'alloc mut Allocator,
97) -> Result<Expression<'alloc>, OxcDiagnostic> {103) -> Result<Expression<'alloc>, OxcDiagnostic> {
98 if source.is_empty() {104 if source.is_empty() {
...@@ -130,7 +136,7 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(...@@ -130,7 +136,7 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(
130 let mut whitespace_groups = 0;136 let mut whitespace_groups = 0;
131 loop {137 loop {
132 if candidate.is_empty() {138 if candidate.is_empty() {
133 return Err(adjust_err(first_err, offset));139 return Err(adjust_err(first_err, offset as isize));
134 }140 }
135 match oxc_parser::Parser::new(allocator, candidate, source_type).parse_expression()141 match oxc_parser::Parser::new(allocator, candidate, source_type).parse_expression()
136 {142 {
...@@ -141,7 +147,7 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(...@@ -141,7 +147,7 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(
141 if after_trim.len() < before_trim.len() {147 if after_trim.len() < before_trim.len() {
142 whitespace_groups += 1;148 whitespace_groups += 1;
143 if whitespace_groups > 1 {149 if whitespace_groups > 1 {
144 return Err(adjust_err(first_err, offset));150 return Err(adjust_err(first_err, offset as isize));
145 }151 }
146 }152 }
147 candidate = after_trim;153 candidate = after_trim;
...@@ -164,17 +170,16 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(...@@ -164,17 +170,16 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(
164}170}
165171
166/// parse ts expression, stopping at garbage data / comma172/// parse ts expression, stopping at garbage data / comma
167pub fn parse_expr(l: &mut LexState) -> Result<usize, OxcDiagnostic> {173pub fn parse_expr(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
168 let mut allocator = Allocator::new();174 let mut allocator = Allocator::new();
169 let expr = parse_expr_extra(l.peek_rest(), l.offset().cast_signed(), &mut allocator)?;175 let expr = parse_expr_extra(l.peek_rest(), l.offset().cast_signed(), &mut allocator)?;
170 let span = expr.span();176 let span = expr.span();
171 let len = (span.end - span.start) as usize;177 l.advance(span.end - span.start);
172 l.advance(len);178 Ok(span.end - span.start)
173 Ok(len)
174}179}
175180
176/// parse ts expression but stop at the first > because of ambiguity with HTML tag end181/// parse ts expression but stop at the first > because of ambiguity with HTML tag end
177pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {182pub fn parse_expr_without_gt(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
178 use oxc_ast::ast::{BinaryOperator, Expression};183 use oxc_ast::ast::{BinaryOperator, Expression};
179184
180 let rest = l.peek_rest();185 let rest = l.peek_rest();
...@@ -196,7 +201,7 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -196,7 +201,7 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
196 }201 }
197 }202 }
198203
199 let length = e.span().end as usize;204 let length = e.span().end;
200 l.advance(length);205 l.advance(length);
201 return Ok(length);206 return Ok(length);
202 }207 }
...@@ -215,9 +220,9 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -215,9 +220,9 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
215 allocator.reset();220 allocator.reset();
216 if let Ok(expr) = parse_expr_extra(candidate, l.offset().cast_signed(), &mut allocator) {221 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)222 // Check that the parse consumed most of the candidate (not just a prefix)
218 let length = expr.span().end as usize;223 let length = expr.span().end;
219 // If the expression spans close to the full candidate, use it224 // If the expression spans close to the full candidate, use it
220 if length >= pos.saturating_sub(1) {225 if length >= (pos.saturating_sub(1) as u32) {
221 l.advance(length);226 l.advance(length);
222 return Ok(length);227 return Ok(length);
223 }228 }
...@@ -227,18 +232,16 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -227,18 +232,16 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
227 // Last resort: return error from the original parse attempt232 // Last resort: return error from the original parse attempt
228 allocator.reset();233 allocator.reset();
229 let expr = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator)?;234 let expr = parse_expr_extra(rest, l.offset().cast_signed(), &mut allocator)?;
230 let length = expr.span().end as usize;235 let length = expr.span().end;
231 l.advance(length);236 l.advance(length);
232 Ok(length)237 Ok(length)
233}238}
234239
235/// parse call arguments including the parentheses: `(a, b, ...c)`240/// parse call arguments including the parentheses: `(a, b, ...c)`
236pub fn parse_call_arguments(l: &mut LexState) -> Result<usize, OxcDiagnostic> {241pub fn parse_call_arguments(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
237 let source = l.peek_rest();242 let source = l.peek_rest();
238 if !source.starts_with("(") {243 if !source.starts_with("(") {
239 return Err(244 return Err(err("Expected `(`", l.offset(), 1));
240 OxcDiagnostic::error("Expected `(`").and_label(LabeledSpan::new(None, l.offset(), 1)),
241 );
242 }245 }
243246
244 // Prepend `f` to make it a call expression: "f(a, b, c)"247 // Prepend `f` to make it a call expression: "f(a, b, c)"
...@@ -248,23 +251,18 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -248,23 +251,18 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
248 let expr = parse_expr_extra(wrapped.as_str(), l.offset().cast_signed(), &mut allocator)?;251 let expr = parse_expr_extra(wrapped.as_str(), l.offset().cast_signed(), &mut allocator)?;
249252
250 // Walk down the left side of the AST to find the CallExpression253 // Walk down the left side of the AST to find the CallExpression
251 let call_span = find_leftmost_call(&expr).ok_or_else(|| {254 let call_span =
252 OxcDiagnostic::error("Expected call expression").and_label(LabeledSpan::new(255 find_leftmost_call(&expr).ok_or_else(|| err("Expected call expression", l.offset(), 1))?;
253 None,
254 l.offset(),
255 1,
256 ))
257 })?;
258256
259 // Subtract the `f` prefix we added257 // Subtract the `f` prefix we added
260 let length = call_span.end as usize - 1;258 let length = call_span.end - 1;
261 l.advance(length);259 l.advance(length);
262 Ok(length)260 Ok(length)
263}261}
264262
265/// parse variable binding. identifier or destructuring pattern263/// parse variable binding. identifier or destructuring pattern
266/// also parses optional `: Type`264/// also parses optional `: Type`
267pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {265pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
268 // TODO: this approach has bugs266 // TODO: this approach has bugs
269 let offset = l.offset();267 let offset = l.offset();
270 let source = l.peek_rest();268 let source = l.peek_rest();
...@@ -276,16 +274,16 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -276,16 +274,16 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
276 let end = {274 let end = {
277 let expr = parse_expr_extra(source, l.offset().cast_signed(), &mut allocator)?;275 let expr = parse_expr_extra(source, l.offset().cast_signed(), &mut allocator)?;
278 let span = find_leftmost(&expr, LeftmostSearch::Assignment).unwrap();276 let span = find_leftmost(&expr, LeftmostSearch::Assignment).unwrap();
279 span.end as usize277 span.end
280 };278 };
281 allocator.reset();279 allocator.reset();
282280
283 {281 {
284 let prefix = "function f(";282 let prefix = "function f(";
285 let source = format!("{prefix}{}) {{}}", &source[0..end]);283 let source = format!("{prefix}{}) {{}}", &source[0..end as usize]);
286 parse_expr_extra(284 parse_expr_extra(
287 source.as_str(),285 source.as_str(),
288 l.offset().cast_signed() - prefix.len().cast_signed(),286 l.offset().cast_signed() - (prefix.len().cast_signed() as i32),
289 &mut allocator,287 &mut allocator,
290 )?;288 )?;
291 }289 }
...@@ -305,7 +303,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -305,7 +303,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
305}303}
306304
307/// parses a type305/// parses a type
308pub fn parse_type(l: &mut LexState) -> Result<usize, OxcDiagnostic> {306pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
309 let mut allocator = Allocator::default();307 let mut allocator = Allocator::default();
310 let source = format!("T as {}", l.peek_rest());308 let source = format!("T as {}", l.peek_rest());
311 println!("{{{source}}} HUH");309 println!("{{{source}}} HUH");
...@@ -319,19 +317,16 @@ pub fn parse_type(l: &mut LexState) -> Result<usize, OxcDiagnostic> {...@@ -319,19 +317,16 @@ pub fn parse_type(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
319 expr.span().end as usize,317 expr.span().end as usize,
320 )318 )
321 })?;319 })?;
322 let len = (span.end - span.start) as usize;320 let len = span.end - span.start;
323
324 l.advance(len);321 l.advance(len);
325 Ok(len)322 Ok(len)
326}323}
327324
328/// parses `(params): ReturnType { body }`325/// parses `(params): ReturnType { body }`
329pub fn parse_fn_params_and_body(l: &mut LexState) -> Result<usize, OxcDiagnostic> {326pub fn parse_fn_params_and_body(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
330 let source = l.peek_rest();327 let source = l.peek_rest();
331 if !source.starts_with("(") {328 if !source.starts_with("(") {
332 return Err(329 return Err(err("Expected `(`", l.offset(), 1));
333 OxcDiagnostic::error("Expected `(`").and_label(LabeledSpan::new(None, l.offset(), 1)),
334 );
335 }330 }
336331
337 // Prepend `function f` to make it a function expression332 // Prepend `function f` to make it a function expression
...@@ -340,19 +335,14 @@ pub fn parse_fn_params_and_body(l: &mut LexState) -> Result<usize, OxcDiagnostic...@@ -340,19 +335,14 @@ pub fn parse_fn_params_and_body(l: &mut LexState) -> Result<usize, OxcDiagnostic
340335
341 let mut allocator = Allocator::default();336 let mut allocator = Allocator::default();
342337
343 let expr = parse_expr_extra(&wrapped, l.offset().cast_signed(), &mut allocator)?;338 let expr = parse_expr_extra(&wrapped, l.offset() as i32, &mut allocator)?;
344339
345 // Find the function expression (walk left side of any binary/etc expressions)340 // Find the function expression (walk left side of any binary/etc expressions)
346 let func_span = find_leftmost(&expr, LeftmostSearch::Function).ok_or_else(|| {341 let func_span = find_leftmost(&expr, LeftmostSearch::Function)
347 OxcDiagnostic::error("Expected function expression").and_label(LabeledSpan::new(342 .ok_or_else(|| err("Expected function expression", l.offset(), 1))?;
348 None,
349 l.offset(),
350 1,
351 ))
352 })?;
353343
354 // Subtract the prefix we added344 // Subtract the prefix we added
355 let length = func_span.end as usize - prefix.len();345 let length = func_span.end - (prefix.len() as u32);
356 l.advance(length);346 l.advance(length);
357 Ok(length)347 Ok(length)
358}348}
...@@ -483,7 +473,7 @@ fn find_leftmost_assign(...@@ -483,7 +473,7 @@ fn find_leftmost_assign(
483mod tests {473mod tests {
484 use super::*;474 use super::*;
485475
486 fn parse_expr(source: &str) -> Result<usize, OxcDiagnostic> {476 fn parse_expr(source: &str) -> Result<u32, OxcDiagnostic> {
487 let mut l = LexState::new(source);477 let mut l = LexState::new(source);
488 super::parse_expr(&mut l)478 super::parse_expr(&mut l)
489 }479 }
...@@ -568,7 +558,7 @@ mod tests {...@@ -568,7 +558,7 @@ mod tests {
568 assert!(result.is_err());558 assert!(result.is_err());
569 }559 }
570560
571 fn parse_expr_no_gt(source: &str) -> Result<usize, OxcDiagnostic> {561 fn parse_expr_no_gt(source: &str) -> Result<u32, OxcDiagnostic> {
572 let mut l = LexState::new(source);562 let mut l = LexState::new(source);
573 parse_expr_without_gt(&mut l)563 parse_expr_without_gt(&mut l)
574 }564 }
...@@ -621,7 +611,7 @@ mod tests {...@@ -621,7 +611,7 @@ mod tests {
621 assert_eq!(parse_expr_no_gt("x >>> 2"), Ok(1));611 assert_eq!(parse_expr_no_gt("x >>> 2"), Ok(1));
622 }612 }
623613
624 fn parse_call_args(source: &str) -> Result<usize, OxcDiagnostic> {614 fn parse_call_args(source: &str) -> Result<u32, OxcDiagnostic> {
625 let mut l = LexState::new(source);615 let mut l = LexState::new(source);
626 parse_call_arguments(&mut l)616 parse_call_arguments(&mut l)
627 }617 }
...@@ -671,7 +661,7 @@ mod tests {...@@ -671,7 +661,7 @@ mod tests {
671 assert!(result.is_err());661 assert!(result.is_err());
672 }662 }
673663
674 fn parse_fn_params_body(source: &str) -> Result<usize, OxcDiagnostic> {664 fn parse_fn_params_body(source: &str) -> Result<u32, OxcDiagnostic> {
675 let mut l = LexState::new(source);665 let mut l = LexState::new(source);
676 parse_fn_params_and_body(&mut l)666 parse_fn_params_and_body(&mut l)
677 }667 }
...@@ -736,7 +726,7 @@ mod tests {...@@ -736,7 +726,7 @@ mod tests {
736 assert!(result.is_err());726 assert!(result.is_err());
737 }727 }
738728
739 fn parse_var_binding(source: &str) -> Result<usize, OxcDiagnostic> {729 fn parse_var_binding(source: &str) -> Result<u32, OxcDiagnostic> {
740 let mut l = LexState::new(source);730 let mut l = LexState::new(source);
741 super::parse_var_binding(&mut l)731 super::parse_var_binding(&mut l)
742 }732 }
...@@ -816,35 +806,41 @@ mod tests {...@@ -816,35 +806,41 @@ mod tests {
816 fn test_stmt_function_with_garbage() {806 fn test_stmt_function_with_garbage() {
817 let source = "function hello() {\n console.log(1);\n}\n\nrandom markdown garbage";807 let source = "function hello() {\n console.log(1);\n}\n\nrandom markdown garbage";
818 let end = scan_first_statement_forbid_trailing(source).unwrap();808 let end = scan_first_statement_forbid_trailing(source).unwrap();
819 assert_eq!(&source[..end], "function hello() {\n console.log(1);\n}");809 assert_eq!(
810 &source[..end as usize],
811 "function hello() {\n console.log(1);\n}"
812 );
820 }813 }
821814
822 #[test]815 #[test]
823 fn test_stmt_import_with_garbage() {816 fn test_stmt_import_with_garbage() {
824 let source = "import { foo } from 'bar';\n\n# markdown heading";817 let source = "import { foo } from 'bar';\n\n# markdown heading";
825 let end = scan_first_statement_forbid_trailing(source).unwrap();818 let end = scan_first_statement_forbid_trailing(source).unwrap();
826 assert_eq!(&source[..end], "import { foo } from 'bar';");819 assert_eq!(&source[..end as usize], "import { foo } from 'bar';");
827 }820 }
828821
829 #[test]822 #[test]
830 fn test_stmt_interface_with_garbage() {823 fn test_stmt_interface_with_garbage() {
831 let source = "interface Foo {\n bar: string;\n}\n\nsome text";824 let source = "interface Foo {\n bar: string;\n}\n\nsome text";
832 let end = scan_first_statement_forbid_trailing(source).unwrap();825 let end = scan_first_statement_forbid_trailing(source).unwrap();
833 assert_eq!(&source[..end], "interface Foo {\n bar: string;\n}");826 assert_eq!(
827 &source[..end as usize],
828 "interface Foo {\n bar: string;\n}"
829 );
834 }830 }
835831
836 #[test]832 #[test]
837 fn test_stmt_const_declaration() {833 fn test_stmt_const_declaration() {
838 let source = "const answer = 42;\n\n# Next section";834 let source = "const answer = 42;\n\n# Next section";
839 let end = scan_first_statement_forbid_trailing(source).unwrap();835 let end = scan_first_statement_forbid_trailing(source).unwrap();
840 assert_eq!(&source[..end], "const answer = 42;");836 assert_eq!(&source[..end as usize], "const answer = 42;");
841 }837 }
842838
843 #[test]839 #[test]
844 fn test_stmt_expression_statement() {840 fn test_stmt_expression_statement() {
845 let source = "console.log('hello');\n\nmore content";841 let source = "console.log('hello');\n\nmore content";
846 let end = scan_first_statement_forbid_trailing(source).unwrap();842 let end = scan_first_statement_forbid_trailing(source).unwrap();
847 assert_eq!(&source[..end], "console.log('hello');");843 assert_eq!(&source[..end as usize], "console.log('hello');");
848 }844 }
849845
850 #[test]846 #[test]
...@@ -858,8 +854,8 @@ mod tests {...@@ -858,8 +854,8 @@ mod tests {
858 fn test_stmt_multiline_function() {854 fn test_stmt_multiline_function() {
859 let source = "function sort(items) {\n while (!isSorted()) {\n shuffle(items);\n }\n return items;\n}\n\n# Heading";855 let source = "function sort(items) {\n while (!isSorted()) {\n shuffle(items);\n }\n return items;\n}\n\n# Heading";
860 let end = scan_first_statement_forbid_trailing(source).unwrap();856 let end = scan_first_statement_forbid_trailing(source).unwrap();
861 assert!(source[..end].contains("return items;"));857 assert!(source[..end as usize].contains("return items;"));
862 assert!(source[..end].contains("}"));858 assert!(source[..end as usize].contains("}"));
863 }859 }
864860
865 #[test]861 #[test]
tests/fixtures/23-outline-extracting.marko+3
...@@ -7,6 +7,9 @@ good night...@@ -7,6 +7,9 @@ good night
7<define/Header_3__markodown__>7<define/Header_3__markodown__>
8<strong>snow</strong> time8<strong>snow</strong> time
9</>9</>
10rain <strong>time</strong>
11<define/Header_4__markodown__>
12</>
10<Layout__markodown__ module=self__markodown__ outline=[13<Layout__markodown__ module=self__markodown__ outline=[
11 { level: 1, id: 'good-morning', content: Header_1__markodown__ },14 { level: 1, id: 'good-morning', content: Header_1__markodown__ },
12 { level: 2, id: 'good-night', content: Header_2__markodown__ },15 { level: 2, id: 'good-night', content: Header_2__markodown__ },