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!
2828 <@img src="IMG_4838.jpeg" w=2 />
2929 <@img src="IMG_4839.jpeg" w=2 align="top" />
3030 <@img src="IMG_4833.jpeg" h=2 />
31 <@img src="IMG_4832.jpeg" w=2 />
3231</>
3332
3433## in conclusion
examples/marko-run/src/tags/photo-grid.marko+1-1
......@@ -46,7 +46,7 @@ export interface Input {
4646 "grid-template-rows": input.rows.map(row => `${row}px`).join(" "),
4747}>
4848 <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};` />
5050 <const/hue = (pos.x + pos.y) * 37 % 360 />
5151 <const/bgColor = `hsl(${hue}, 65%, 75%)` />
5252 <div class="photo-grid-item" style=`${itemStyle} background: ${bgColor};` />
src/lib.rs+46-82
......@@ -1,10 +1,11 @@
11pub mod marko;
2pub mod marko_ast;
23pub mod plugin;
34pub mod typescript;
4
55pub mod wasm;
66
77use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
8use oxc_span::Span;
89use plugin::tags::{MarkoClose, MarkoOpen};
910use serde::Serialize;
1011use serde_json;
......@@ -94,7 +95,7 @@ pub fn transform(
9495 };
9596
9697 // 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
99100 let mut text = ast.render();
100101
......@@ -177,9 +178,9 @@ pub fn adjust_err(mut err: OxcDiagnostic, offset: isize) -> OxcDiagnostic {
177178 err
178179}
179180
180/// Extract all StatementBlock content from the AST and clear them.
181/// Returns the collected statement content as a single string.
182fn extract_statements(node: &mut markdown_it::Node) -> String {
181/// Extract `StatementBlock` contents from the AST, replacing them with an empty
182/// string so they render as nothing. Returns the statements hoisted.
183fn hoist_statements(node: &mut markdown_it::Node) -> String {
183184 let mut statements = String::new();
184185
185186 fn walk(node: &mut markdown_it::Node, statements: &mut String) {
......@@ -198,33 +199,15 @@ fn extract_statements(node: &mut markdown_it::Node) -> String {
198199
199200/// Check if the AST contains any Marko-specific features
200201fn has_marko_features(node: &markdown_it::Node) -> bool {
201 // Check if this node is a RawBlock or StatementBlock
202 if node.cast::<plugin::RawBlock>().is_some() {
203 return true;
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()
202 node.cast::<plugin::RawBlock>().is_some()
203 || node.cast::<plugin::StatementBlock>().is_some()
204 || node.cast::<MarkoOpen>().is_some()
211205 || node.cast::<MarkoClose>().is_some()
212206 || node.cast::<plugin::tags::MarkoSelfClosing>().is_some()
213207 || node.cast::<plugin::tags::MarkoOpenWithText>().is_some()
214208 || node.cast::<plugin::tags::MarkoInlineTag>().is_some()
215209 || node.cast::<plugin::tags::MarkoBlockComplete>().is_some()
216 {
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
210 || node.children.iter().any(|child| has_marko_features(child))
228211}
229212
230213/// Validate that Marko open/close tags are properly matched
......@@ -233,68 +216,57 @@ fn validate_marko_tags(
233216 errors: &mut Vec<OxcDiagnostic>,
234217 preamble_offset: usize,
235218) {
236 #[derive(Debug)]
237 struct OpenTag {
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![];
219 // Stack of (tag_name, absolute_span_of_name)
220 let mut stack: Vec<(String, Span)> = vec![];
246221
247222 fn walk(
248223 node: &markdown_it::Node,
249 stack: &mut Vec<OpenTag>,
224 stack: &mut Vec<(String, Span)>,
250225 errors: &mut Vec<OxcDiagnostic>,
251 offset: usize,
226 offset: u32,
252227 ) {
253228 if let Some(open) = node.cast::<MarkoOpen>() {
254229 let (start, _) = node.srcmap.unwrap().get_byte_offsets();
255 stack.push(OpenTag {
256 name: open.tag_name.clone(),
257 name_start: start + offset + 1, // +1 to skip '<'
258 name_len: open.tag_name_len,
259 });
230 let rel_span = open.open.as_ref().tag_name_span();
231 let abs_span = Span::new(
232 start as u32 + offset + rel_span.start,
233 start as u32 + offset + rel_span.end,
234 );
235 stack.push((open.open.as_ref().tag_name().to_owned(), abs_span));
260236 } else if let Some(open) = node.cast::<plugin::tags::MarkoOpenWithText>() {
261237 let (start, _) = node.srcmap.unwrap().get_byte_offsets();
262 stack.push(OpenTag {
263 name: open.tag_name.clone(),
264 name_start: start + offset + 1, // +1 to skip '<'
265 name_len: open.tag_name_len,
266 });
238 let rel_span = open.open.as_ref().tag_name_span();
239 let abs_span = Span::new(
240 start as u32 + offset + rel_span.start,
241 start as u32 + offset + rel_span.end,
242 );
243 stack.push((open.open.as_ref().tag_name().to_owned(), abs_span));
267244 } else if let Some(close) = node.cast::<MarkoClose>() {
268245 let (close_start, _) = node.srcmap.unwrap().get_byte_offsets();
246 let close_start = close_start as u32 + offset;
269247 // Close tag name starts after '</' (offset +2)
270 let close_name_start = close_start + offset + 2;
271 let close_name_len = close.tag_name_len.unwrap_or(0);
248 let close_span = close
249 .tag_name
250 .as_ref()
251 .map(|name| Span::new(close_start + 2, close_start + 2 + name.len() as u32));
272252
273253 match (close.tag_name.as_ref(), stack.pop()) {
254 (Some(name), Some((top_name, _))) if name == &top_name => {
255 // valid
256 }
274257 (None, Some(_)) => {
275258 // </> closes anything - valid
276259 }
277 (Some(name), Some(top)) if name == &top.name => {
278 // Exact match - valid
279 }
280 (Some(name), Some(top)) => {
281 // Mismatched: expected </top.name>, got </name>
260 (Some(name), Some((top_name, top_span))) => {
261 // Mismatched: expected </top_name>, got </name>
282262 errors.push(
283263 OxcDiagnostic::error(format!(
284264 "Mismatched closing tag: expected </{}>, found </{}>",
285 top.name, name
265 top_name, name
286266 ))
287267 .with_labels(vec![
288 LabeledSpan::new(
289 Some("opened here".into()),
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 ),
268 top_span.label("opened here"),
269 close_span.unwrap().label("closed here"),
298270 ]),
299271 );
300272 }
......@@ -304,18 +276,14 @@ fn validate_marko_tags(
304276 OxcDiagnostic::error(format!(
305277 "Closing tag </{name}> without matching open"
306278 ))
307 .with_label(LabeledSpan::new(
308 None,
309 close_name_start,
310 close_name_len,
311 )),
279 .with_label(close_span.unwrap()),
312280 );
313281 }
314282 (None, None) => {
315283 // </> without any open tag - highlight the whole </>
316284 errors.push(
317285 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)),
319287 );
320288 }
321289 }
......@@ -327,18 +295,14 @@ fn validate_marko_tags(
327295 }
328296 }
329297
330 walk(node, &mut stack, errors, preamble_offset);
298 walk(node, &mut stack, errors, preamble_offset as u32);
331299
332300 // Check for unclosed tags
333 for unclosed in stack {
334 errors.push(
335 OxcDiagnostic::error(format!("Unclosed tag <{}>", unclosed.name)).with_label(
336 LabeledSpan::new(None, unclosed.name_start, unclosed.name_len),
337 ),
338 );
301 for (name, span) in stack {
302 errors.push(OxcDiagnostic::error(format!("Unclosed tag <{}>", name)).with_label(span));
339303 }
340304}
341305
342pub fn err<T: Into<Cow<'static, str>>>(str: T, offset: usize, length: usize) -> OxcDiagnostic {
343 OxcDiagnostic::error(str).and_label(LabeledSpan::new(None, offset, length))
306pub fn err<T: Into<Cow<'static, str>>>(str: T, offset: u32, length: usize) -> OxcDiagnostic {
307 OxcDiagnostic::error(str).and_label(LabeledSpan::new(None, offset as usize, length))
344308}
src/marko.rs+334-629
......@@ -1,74 +1,16 @@
1use std::{borrow::Cow, iter::Map};
2
31use oxc_diagnostics::OxcDiagnostic;
42use oxc_span::Span;
3use serde_yml::libyml::tag;
54
65use crate::{
76 err,
7 marko_ast::*,
88 typescript::{
99 parse_call_arguments, parse_expr, parse_expr_without_gt, parse_fn_params_and_body,
1010 parse_var_binding,
1111 },
1212};
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
7214pub fn parse_tag(src: &str) -> Result<OpenOrClose, OxcDiagnostic> {
7315 assert_eq!(src.as_bytes()[0], b'<');
7416 if src.starts_with("</") {
......@@ -78,66 +20,6 @@ pub fn parse_tag(src: &str) -> Result<OpenOrClose, OxcDiagnostic> {
7820 }
7921}
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
14123pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
14224 assert!(src.starts_with("<"));
14325 let mut l = LexState { src, offset: 1 };
......@@ -145,21 +27,33 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
14527 let is_attribute_tag = tag_name.starts_with('@');
14628
14729 // class and id shorthand
148 let mut has_id = false;
30 let mut id_attr = AttributeValue::None;
14931 while let Some(byte @ b'#' | byte @ b'.') = l.peek_byte() {
150 let offset = l.offset;
32 let id_start = l.offset;
15133 l.advance(1);
15234 let name = parse_class_name_shorthand(&mut l, byte == b'#')?;
153 _ = name;
35 let id_end = l.offset;
15436
15537 if byte == b'#' {
156 if has_id {
157 return Err(err("Cannot specify two ID shorthands", offset, name.len()));
38 if !matches!(id_attr, AttributeValue::None) {
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 };
15850 }
159 has_id = true;
16051 }
16152 }
16253
54 // Track where shorthands end (for inserting new #id if needed)
55 let shorthand_end = l.offset;
56
16357 // in any order: parameters, arguments, variable
16458 let mut has_js_arguments = false;
16559 let mut has_js_params = false;
......@@ -239,14 +133,14 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
239133 return Err(err(
240134 "Attribute tags do not support variables",
241135 offset,
242 l.offset - offset,
136 (l.offset - offset) as usize,
243137 ));
244138 }
245139 if is_attribute_tag && byte == b'(' {
246140 return Err(err(
247141 "Attribute tags do not support arguments",
248142 offset,
249 l.offset - offset,
143 (l.offset - offset) as usize,
250144 ));
251145 }
252146 }
......@@ -274,8 +168,36 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
274168 self_closing = true;
275169 break;
276170 }
277 parse_attribute(&mut l)?;
278 l.skip_whitespace();
171 // Check for id= attribute (only if we don't already have an id)
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 }
279201 }
280202 }
281203 }
......@@ -287,11 +209,13 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
287209 }
288210 }
289211
290 Ok(Open {
291 tag_name,
292 content: &src[0..l.offset],
212 Ok(Open::new(
213 &src[0..l.offset as usize],
214 Span::new(1, (tag_name.len() + 1) as u32),
293215 self_closing,
294 })
216 shorthand_end,
217 id_attr,
218 ))
295219}
296220
297221pub 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> {
329253 _ => break,
330254 }
331255 }
332 let tag_name = &l.src[start..l.offset];
256 let tag_name = &l.src[start as usize..l.offset as usize];
333257 if !tag_name.is_empty() {
334258 Ok(tag_name)
335259 } else {
......@@ -345,7 +269,7 @@ fn parse_attr_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> {
345269 _ => break,
346270 }
347271 }
348 let tag_name = &l.src[start..l.offset];
272 let tag_name = &l.src[start as usize..l.offset as usize];
349273 if !tag_name.is_empty() {
350274 Ok(tag_name)
351275 } else {
......@@ -408,7 +332,7 @@ fn parse_class_name_shorthand<'a>(
408332 _ => l.offset += 1,
409333 }
410334 }
411 let tag_name = &l.src[start..l.offset];
335 let tag_name = &l.src[start as usize..l.offset as usize];
412336 if !tag_name.is_empty() {
413337 Ok(tag_name)
414338 } else {
......@@ -428,133 +352,73 @@ fn parse_class_name_shorthand<'a>(
428352mod tests {
429353 use super::*;
430354
431 #[test]
432 fn test_open_basic() {
355 /// Helper to check basic Open properties without worrying about id_info/shorthand_end
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}");
433360 assert_eq!(
434 parse_open("<div>"),
435 Ok(Open {
436 tag_name: "div",
437 content: "<div>",
438 self_closing: false,
439 })
361 open.self_closing, self_closing,
362 "self_closing mismatch for {src}"
440363 );
441 assert_eq!(
442 parse_open("<footer#id.class meow=purr yolo=4.2 /> etfdas"),
443 Ok(Open {
444 tag_name: "footer",
445 content: "<footer#id.class meow=purr yolo=4.2 />",
446 self_closing: true,
447 })
364 }
365
366 #[test]
367 fn test_open_basic() {
368 check_open("<div>", "div", "<div>", false);
369 check_open(
370 "<footer#id.class meow=purr yolo=4.2 /> etfdas",
371 "footer",
372 "<footer#id.class meow=purr yolo=4.2 />",
373 true,
448374 );
449 assert_eq!(
450 parse_open("<movie value=x > 4 /> etfdas"),
451 Ok(Open {
452 tag_name: "movie",
453 content: "<movie value=x >",
454 self_closing: false,
455 })
375 check_open(
376 "<movie value=x > 4 /> etfdas",
377 "movie",
378 "<movie value=x >",
379 false,
456380 );
457381 }
458382
459383 #[test]
460384 fn test_open_id() {
461 assert_eq!(
462 parse_open("<h2#technical-review>A Technical Review"),
463 Ok(Open {
464 tag_name: "h2",
465 content: "<h2#technical-review>",
466 self_closing: false,
467 })
385 check_open(
386 "<h2#technical-review>A Technical Review",
387 "h2",
388 "<h2#technical-review>",
389 false,
468390 );
469 assert_eq!(
470 parse_open("<footer#id.class meow=purr yolo=4.2 /> etfdas"),
471 Ok(Open {
472 tag_name: "footer",
473 content: "<footer#id.class meow=purr yolo=4.2 />",
474 self_closing: true,
475 })
391 check_open(
392 "<footer#id.class meow=purr yolo=4.2 /> etfdas",
393 "footer",
394 "<footer#id.class meow=purr yolo=4.2 />",
395 true,
476396 );
477 assert_eq!(
478 parse_open("<movie value=x > 4 /> etfdas"),
479 Ok(Open {
480 tag_name: "movie",
481 content: "<movie value=x >",
482 self_closing: false,
483 })
397 check_open(
398 "<movie value=x > 4 /> etfdas",
399 "movie",
400 "<movie value=x >",
401 false,
484402 );
485403 }
486404
487405 #[test]
488406 fn test_open_self_closing_with_space() {
489 // Self-closing with space before /> works
490 assert_eq!(
491 parse_open("<input />"),
492 Ok(Open {
493 tag_name: "input",
494 content: "<input />",
495 self_closing: true,
496 })
497 );
407 check_open("<input />", "input", "<input />", true);
498408 }
499409
500410 #[test]
501411 fn test_open_self_closing_no_space() {
502 assert_eq!(
503 parse_open("<br/>"),
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 );
412 check_open("<br/>", "br", "<br/>", true);
413 check_open("<my-component/>", "my-component", "<my-component/>", true);
518414 }
519415
520416 #[test]
521417 fn test_open_tag_names() {
522 // kebab-case
523 assert_eq!(
524 parse_open("<my-custom-tag>"),
525 Ok(Open {
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 );
418 check_open("<my-custom-tag>", "my-custom-tag", "<my-custom-tag>", false);
419 check_open("<MyComponent>", "MyComponent", "<MyComponent>", false);
420 check_open("<my_tag>", "my_tag", "<my_tag>", false);
421 check_open("<$tag>", "$tag", "<$tag>", false);
558422 }
559423
560424 #[test]
......@@ -602,53 +466,32 @@ mod tests {
602466
603467 #[test]
604468 fn test_attribute_tag_basic_open() {
605 // @attr tag without self-closing works
606 assert_eq!(
607 parse_open("<@header>"),
608 Ok(Open {
609 tag_name: "@header",
610 content: "<@header>",
611 self_closing: false,
612 })
613 );
469 check_open("<@header>", "@header", "<@header>", false);
614470 }
615471
616472 #[test]
617473 fn test_attribute_tag_self_closing() {
618 assert_eq!(
619 parse_open("<@item/>"),
620 Ok(Open {
621 tag_name: "@item",
622 content: "<@item/>",
623 self_closing: true,
624 })
625 );
474 check_open("<@item/>", "@item", "<@item/>", true);
626475 }
627476
628477 #[test]
629478 fn test_attribute_tag_with_string_attr() {
630 assert_eq!(
631 parse_open("<@option value=\"foo\">"),
632 Ok(Open {
633 tag_name: "@option",
634 content: "<@option value=\"foo\">",
635 self_closing: false,
636 })
479 check_open(
480 "<@option value=\"foo\">",
481 "@option",
482 "<@option value=\"foo\">",
483 false,
637484 );
638485 }
639486
640487 #[test]
641488 fn test_attribute_tag_no_variable() {
642 // Attribute tags do not support tag variables - correctly errors
643 let result = parse_open("<@header/myVar>");
644 assert!(result.is_err());
489 assert!(parse_open("<@header/myVar>").is_err());
645490 }
646491
647492 #[test]
648493 fn test_attribute_tag_no_arguments() {
649 // Attribute tags do not support tag arguments - correctly errors
650 let result = parse_open("<@header(arg)>");
651 assert!(result.is_err());
494 assert!(parse_open("<@header(arg)>").is_err());
652495 }
653496
654497 // ===========================================
......@@ -657,97 +500,47 @@ mod tests {
657500
658501 #[test]
659502 fn test_shorthand_id() {
660 assert_eq!(
661 parse_open("<div#myId>"),
662 Ok(Open {
663 tag_name: "div",
664 content: "<div#myId>",
665 self_closing: false,
666 })
667 );
503 check_open("<div#myId>", "div", "<div#myId>", false);
668504 }
669505
670506 #[test]
671507 fn test_shorthand_class() {
672 assert_eq!(
673 parse_open("<div.myClass>"),
674 Ok(Open {
675 tag_name: "div",
676 content: "<div.myClass>",
677 self_closing: false,
678 })
679 );
508 check_open("<div.myClass>", "div", "<div.myClass>", false);
680509 }
681510
682511 #[test]
683512 fn test_shorthand_multiple_classes() {
684 assert_eq!(
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 );
513 check_open("<div.cls1.cls2.cls3>", "div", "<div.cls1.cls2.cls3>", false);
692514 }
693515
694516 #[test]
695517 fn test_shorthand_id_and_classes() {
696 assert_eq!(
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 );
518 check_open("<div#myId.cls1.cls2>", "div", "<div#myId.cls1.cls2>", false);
704519 }
705520
706521 #[test]
707522 fn test_shorthand_classes_then_id() {
708 // Order shouldn't matter
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 );
523 check_open("<div.cls1#myId.cls2>", "div", "<div.cls1#myId.cls2>", false);
717524 }
718525
719526 #[test]
720527 fn test_shorthand_duplicate_id_error() {
721 // Cannot have two ID shorthands - correctly errors
722 let result = parse_open("<div#id1#id2>");
723 assert!(result.is_err());
528 assert!(parse_open("<div#id1#id2>").is_err());
724529 }
725530
726531 #[test]
727532 fn test_shorthand_with_hyphen() {
728 // Classes/IDs can contain hyphens
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 );
533 check_open("<div.my-class#my-id>", "div", "<div.my-class#my-id>", false);
737534 }
738535
739536 #[test]
740537 fn test_shorthand_empty_class_error() {
741 // Empty class correctly errors
742 let result = parse_open("<div.>");
743 assert!(result.is_err());
538 assert!(parse_open("<div.>").is_err());
744539 }
745540
746541 #[test]
747542 fn test_shorthand_empty_id_error() {
748 // Empty id correctly errors
749 let result = parse_open("<div#>");
750 assert!(result.is_err());
543 assert!(parse_open("<div#>").is_err());
751544 }
752545
753546 // ===========================================
......@@ -756,475 +549,309 @@ mod tests {
756549
757550 #[test]
758551 fn test_tag_variable_simple() {
759 assert_eq!(
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 );
552 check_open("<my-tag/foo>", "my-tag", "<my-tag/foo>", false);
767553 }
768554
769 // BUG: Variable binding followed by /> fails
770555 #[test]
771556 fn test_tag_variable_self_closing() {
772 assert_eq!(
773 parse_open("<input/myInput/>"),
774 Ok(Open {
775 tag_name: "input",
776 content: "<input/myInput/>",
777 self_closing: true,
778 })
779 );
557 check_open("<input/myInput/>", "input", "<input/myInput/>", true);
780558 }
781559
782560 #[test]
783561 fn test_tag_variable_destructure_object() {
784 assert_eq!(
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 );
562 check_open("<my-tag/{ a, b }>", "my-tag", "<my-tag/{ a, b }>", false);
792563 }
793564
794565 #[test]
795566 fn test_tag_variable_destructure_array() {
796 assert_eq!(
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 );
567 check_open("<my-tag/[x, y]>", "my-tag", "<my-tag/[x, y]>", false);
804568 }
805569
806570 #[test]
807571 fn test_tag_variable_with_type() {
808 assert_eq!(
809 parse_open("<const/items: Item[]>"),
810 Ok(Open {
811 tag_name: "const",
812 content: "<const/items: Item[]>",
813 self_closing: false,
814 })
572 check_open(
573 "<const/items: Item[]>",
574 "const",
575 "<const/items: Item[]>",
576 false,
815577 );
816578 }
817579
818580 #[test]
819581 fn test_tag_variable_with_attrs() {
820 assert_eq!(
821 parse_open("<my-tag/result value=42>"),
822 Ok(Open {
823 tag_name: "my-tag",
824 content: "<my-tag/result value=42>",
825 self_closing: false,
826 })
582 check_open(
583 "<my-tag/result value=42>",
584 "my-tag",
585 "<my-tag/result value=42>",
586 false,
827587 );
828588 }
829589
830590 // ===========================================
831591 // 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 (
835592 // ===========================================
836593
837594 #[test]
838595 fn test_tag_arguments_empty() {
839 assert_eq!(
840 parse_open("<my-tag()>"),
841 Ok(Open {
842 tag_name: "my-tag",
843 content: "<my-tag()>",
844 self_closing: false,
845 })
846 );
596 check_open("<my-tag()>", "my-tag", "<my-tag()>", false);
847597 }
848598
849599 #[test]
850600 fn test_tag_arguments_simple() {
851 assert_eq!(
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 );
601 check_open("<my-tag(1, 2, 3)>", "my-tag", "<my-tag(1, 2, 3)>", false);
859602 }
860603
861604 #[test]
862605 fn test_tag_arguments_expressions() {
863 assert_eq!(
864 parse_open("<my-tag(a + b, fn())>"),
865 Ok(Open {
866 tag_name: "my-tag",
867 content: "<my-tag(a + b, fn())>",
868 self_closing: false,
869 })
606 check_open(
607 "<my-tag(a + b, fn())>",
608 "my-tag",
609 "<my-tag(a + b, fn())>",
610 false,
870611 );
871612 }
872613
873614 #[test]
874615 fn test_tag_arguments_spread() {
875 assert_eq!(
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 );
616 check_open("<my-tag(...args)>", "my-tag", "<my-tag(...args)>", false);
883617 }
884618
885619 #[test]
886620 fn test_tag_arguments_object() {
887 assert_eq!(
888 parse_open("<my-tag({ a: 1, b: 2 })>"),
889 Ok(Open {
890 tag_name: "my-tag",
891 content: "<my-tag({ a: 1, b: 2 })>",
892 self_closing: false,
893 })
621 check_open(
622 "<my-tag({ a: 1, b: 2 })>",
623 "my-tag",
624 "<my-tag({ a: 1, b: 2 })>",
625 false,
894626 );
895627 }
896628
897 // Test that tag arguments WITH whitespace work
898629 #[test]
899630 fn test_tag_arguments_with_space() {
900 assert_eq!(
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 );
631 check_open("<my-tag (1, 2, 3)>", "my-tag", "<my-tag (1, 2, 3)>", false);
908632 }
909633
910634 // ===========================================
911635 // Attributes
912636 // ===========================================
913637
914 // BUG: String literals fail - expression parser doesn't terminate at > after string
915638 #[test]
916639 fn test_attr_simple_string() {
917 assert_eq!(
918 parse_open("<div class=\"foo\">"),
919 Ok(Open {
920 tag_name: "div",
921 content: "<div class=\"foo\">",
922 self_closing: false,
923 })
924 );
640 check_open("<div class=\"foo\">", "div", "<div class=\"foo\">", false);
925641 }
926642
927643 #[test]
928644 fn test_attr_expression() {
929 // Identifier expressions work
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 );
645 check_open("<div count=1 + 1>", "div", "<div count=1 + 1>", false);
938646 }
939647
940 // BUG: Template literals fail
941648 #[test]
942649 fn test_attr_template_literal() {
943 assert_eq!(
944 parse_open("<div class=`hello ${name}`>"),
945 Ok(Open {
946 tag_name: "div",
947 content: "<div class=`hello ${name}`>",
948 self_closing: false,
949 })
650 check_open(
651 "<div class=`hello ${name}`>",
652 "div",
653 "<div class=`hello ${name}`>",
654 false,
950655 );
951656 }
952657
953658 #[test]
954659 fn test_attr_spread() {
955 assert_eq!(
956 parse_open("<div ...props>"),
957 Ok(Open {
958 tag_name: "div",
959 content: "<div ...props>",
960 self_closing: false,
961 })
962 );
660 check_open("<div ...props>", "div", "<div ...props>", false);
963661 }
964662
965 // BUG: Object literals with > in them fail
966663 #[test]
967664 fn test_attr_spread_object() {
968 assert_eq!(
969 parse_open("<div ...{ a: 1, b: 2 }>"),
970 Ok(Open {
971 tag_name: "div",
972 content: "<div ...{ a: 1, b: 2 }>",
973 self_closing: false,
974 })
665 check_open(
666 "<div ...{ a: 1, b: 2 }>",
667 "div",
668 "<div ...{ a: 1, b: 2 }>",
669 false,
975670 );
976671 }
977672
978 // BUG: Multiple attributes with string values fail
979673 #[test]
980674 fn test_attr_multiple_spreads() {
981 assert_eq!(
982 parse_open("<div ...a ...b foo=\"bar\">"),
983 Ok(Open {
984 tag_name: "div",
985 content: "<div ...a ...b foo=\"bar\">",
986 self_closing: false,
987 })
675 check_open(
676 "<div ...a ...b foo=\"bar\">",
677 "div",
678 "<div ...a ...b foo=\"bar\">",
679 false,
988680 );
989681 }
990682
991683 #[test]
992684 fn test_attr_two_way_binding() {
993 assert_eq!(
994 parse_open("<counter value:=count>"),
995 Ok(Open {
996 tag_name: "counter",
997 content: "<counter value:=count>",
998 self_closing: false,
999 })
685 check_open(
686 "<counter value:=count>",
687 "counter",
688 "<counter value:=count>",
689 false,
1000690 );
1001691 }
1002692
1003693 #[test]
1004694 fn test_attr_two_way_binding_property() {
1005 assert_eq!(
1006 parse_open("<counter value:=input.count>"),
1007 Ok(Open {
1008 tag_name: "counter",
1009 content: "<counter value:=input.count>",
1010 self_closing: false,
1011 })
695 check_open(
696 "<counter value:=input.count>",
697 "counter",
698 "<counter value:=input.count>",
699 false,
1012700 );
1013701 }
1014702
1015703 #[test]
1016704 fn test_attr_method_shorthand() {
1017 assert_eq!(
1018 parse_open("<button onClick(e) { console.log(e) }>"),
1019 Ok(Open {
1020 tag_name: "button",
1021 content: "<button onClick(e) { console.log(e) }>",
1022 self_closing: false,
1023 })
705 check_open(
706 "<button onClick(e) { console.log(e) }>",
707 "button",
708 "<button onClick(e) { console.log(e) }>",
709 false,
1024710 );
1025711 }
1026712
1027713 #[test]
1028714 fn test_attr_method_with_return_type() {
1029 assert_eq!(
1030 parse_open("<button onClick(e): void { console.log(e) }>"),
1031 Ok(Open {
1032 tag_name: "button",
1033 content: "<button onClick(e): void { console.log(e) }>",
1034 self_closing: false,
1035 })
715 check_open(
716 "<button onClick(e): void { console.log(e) }>",
717 "button",
718 "<button onClick(e): void { console.log(e) }>",
719 false,
1036720 );
1037721 }
1038722
1039723 #[test]
1040724 fn test_attr_boolean() {
1041 assert_eq!(
1042 parse_open("<input type=\"checkbox\" checked>"),
1043 Ok(Open {
1044 tag_name: "input",
1045 content: "<input type=\"checkbox\" checked>",
1046 self_closing: false,
1047 })
725 check_open(
726 "<input type=\"checkbox\" checked>",
727 "input",
728 "<input type=\"checkbox\" checked>",
729 false,
1048730 );
1049731 }
1050732
1051733 #[test]
1052734 fn test_attr_boolean_then_attr() {
1053 assert_eq!(
1054 parse_open("<input checked foo=1>"),
1055 Ok(Open {
1056 tag_name: "input",
1057 content: "<input checked foo=1>",
1058 self_closing: false,
1059 })
735 check_open(
736 "<input checked foo=1>",
737 "input",
738 "<input checked foo=1>",
739 false,
1060740 );
1061741 }
1062742
1063743 #[test]
1064744 fn test_attr_with_colon() {
1065 assert_eq!(
1066 parse_open("<div data:value=\"test\">"),
1067 Ok(Open {
1068 tag_name: "div",
1069 content: "<div data:value=\"test\">",
1070 self_closing: false,
1071 })
745 check_open(
746 "<div data:value=\"test\">",
747 "div",
748 "<div data:value=\"test\">",
749 false,
1072750 );
1073751 }
1074752
1075753 #[test]
1076754 fn test_shorthand_value() {
1077 assert_eq!(
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 );
755 check_open("<my-tag=1>", "my-tag", "<my-tag=1>", false);
1085756 }
1086757
1087758 #[test]
1088759 fn test_shorthand_value_expression() {
1089 assert_eq!(
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 );
760 check_open("<my-tag=a + b>", "my-tag", "<my-tag=a + b>", false);
1097761 }
1098762
1099763 #[test]
1100764 fn test_shorthand_value_with_other_attrs() {
1101 assert_eq!(
1102 parse_open("<my-tag=42 foo=\"bar\">"),
1103 Ok(Open {
1104 tag_name: "my-tag",
1105 content: "<my-tag=42 foo=\"bar\">",
1106 self_closing: false,
1107 })
765 check_open(
766 "<my-tag=42 foo=\"bar\">",
767 "my-tag",
768 "<my-tag=42 foo=\"bar\">",
769 false,
1108770 );
1109771 }
1110772
1111773 #[test]
1112774 fn test_value_method_shorthand() {
1113 assert_eq!(
1114 parse_open("<my-tag() { console.log(\"hi\") }>"),
1115 Ok(Open {
1116 tag_name: "my-tag",
1117 content: "<my-tag() { console.log(\"hi\") }>",
1118 self_closing: false,
1119 })
775 check_open(
776 "<my-tag() { console.log(\"hi\") }>",
777 "my-tag",
778 "<my-tag() { console.log(\"hi\") }>",
779 false,
1120780 );
1121781 }
1122782
1123783 #[test]
1124784 fn test_value_method_shorthand_self_closing() {
1125 assert_eq!(
1126 parse_open("<my-tag() { console.log(\"hi\") }/>"),
1127 Ok(Open {
1128 tag_name: "my-tag",
1129 content: "<my-tag() { console.log(\"hi\") }/>",
1130 self_closing: true,
1131 })
785 check_open(
786 "<my-tag() { console.log(\"hi\") }/>",
787 "my-tag",
788 "<my-tag() { console.log(\"hi\") }/>",
789 true,
1132790 );
1133791 }
1134792
1135793 #[test]
1136794 fn test_value_method_shorthand_with_param() {
1137 assert_eq!(
1138 parse_open("<my-tag(e) { console.log(e) }>"),
1139 Ok(Open {
1140 tag_name: "my-tag",
1141 content: "<my-tag(e) { console.log(e) }>",
1142 self_closing: false,
1143 })
795 check_open(
796 "<my-tag(e) { console.log(e) }>",
797 "my-tag",
798 "<my-tag(e) { console.log(e) }>",
799 false,
1144800 );
1145801 }
1146802
1147803 #[test]
1148804 fn test_combined_variable_and_args() {
1149 assert_eq!(
1150 parse_open("<for/item (items)>"),
1151 Ok(Open {
1152 tag_name: "for",
1153 content: "<for/item (items)>",
1154 self_closing: false,
1155 })
1156 );
805 check_open("<for/item (items)>", "for", "<for/item (items)>", false);
1157806 }
1158807
1159808 #[test]
1160809 fn test_combined_id_class_variable() {
1161 assert_eq!(
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 );
810 check_open("<div#myId.cls/ref>", "div", "<div#myId.cls/ref>", false);
1169811 }
1170812
1171813 #[test]
1172814 fn test_combined_complex() {
1173 assert_eq!(
1174 parse_open("<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>"),
1175 Ok(Open {
1176 tag_name: "my-tag",
1177 content: "<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>",
1178 self_closing: false,
1179 })
815 check_open(
816 "<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>",
817 "my-tag",
818 "<my-tag#id.cls/result|arg1, arg2| foo=bar ...spread>",
819 false,
1180820 );
1181821 }
1182822
1183823 #[test]
1184824 fn test_attr_with_gt_in_parens() {
1185 assert_eq!(
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 );
825 check_open("<div value=(a > b)>", "div", "<div value=(a > b)>", false);
1193826 }
1194827
1195828 #[test]
1196829 fn test_attr_with_gt_in_brackets() {
1197 assert_eq!(
1198 parse_open("<div value=arr[a > 0]>"),
1199 Ok(Open {
1200 tag_name: "div",
1201 content: "<div value=arr[a > 0]>",
1202 self_closing: false,
1203 })
830 check_open(
831 "<div value=arr[a > 0]>",
832 "div",
833 "<div value=arr[a > 0]>",
834 false,
1204835 );
1205836 }
1206837
1207838 #[test]
1208839 fn test_attr_arrow_function() {
1209 assert_eq!(
1210 parse_open("<div onClick=() => console.log(1)>"),
1211 Ok(Open {
1212 tag_name: "div",
1213 content: "<div onClick=() => console.log(1)>",
1214 self_closing: false,
1215 })
840 check_open(
841 "<div onClick=() => console.log(1)>",
842 "div",
843 "<div onClick=() => console.log(1)>",
844 false,
1216845 );
1217846 }
1218847
1219848 #[test]
1220849 fn test_attr_ternary() {
1221 assert_eq!(
1222 parse_open("<div class=isActive ? a : b> > > > >"),
1223 Ok(Open {
1224 tag_name: "div",
1225 content: "<div class=isActive ? a : b>",
1226 self_closing: false,
1227 })
850 check_open(
851 "<div class=isActive ? a : b> > > > >",
852 "div",
853 "<div class=isActive ? a : b>",
854 false,
1228855 );
1229856 }
1230857
......@@ -1251,7 +878,7 @@ mod tests {
1251878 let result = parse_tag("<div>").unwrap();
1252879 match result {
1253880 OpenOrClose::Open(open) => {
1254 assert_eq!(open.tag_name, "div");
881 assert_eq!(open.tag_name(), "div");
1255882 }
1256883 OpenOrClose::Close(_) => panic!("Expected Open"),
1257884 }
......@@ -1360,7 +987,7 @@ mod tests {
1360987 let result = parse_open("<div.color-${iconName}/>");
1361988 assert!(result.is_ok());
1362989 let open = result.unwrap();
1363 assert_eq!(open.content, "<div.color-${iconName}/>");
990 assert_eq!(open.src, "<div.color-${iconName}/>");
1364991 }
1365992
1366993 #[test]
......@@ -1368,7 +995,7 @@ mod tests {
1368995 let result = parse_open("<div#${myId}>");
1369996 assert!(result.is_ok());
1370997 let open = result.unwrap();
1371 assert_eq!(open.content, "<div#${myId}>");
998 assert_eq!(open.src, "<div#${myId}>");
1372999 }
13731000
13741001 #[test]
......@@ -1376,7 +1003,7 @@ mod tests {
13761003 let result = parse_open("<button.variant-${meow}#${id}>");
13771004 assert!(result.is_ok());
13781005 let open = result.unwrap();
1379 assert_eq!(open.content, "<button.variant-${meow}#${id}>");
1006 assert_eq!(open.src, "<button.variant-${meow}#${id}>");
13801007 }
13811008
13821009 #[test]
......@@ -1384,7 +1011,7 @@ mod tests {
13841011 let result = parse_open("<div.class1.${dynamic}.class3>");
13851012 assert!(result.is_ok());
13861013 let open = result.unwrap();
1387 assert_eq!(open.content, "<div.class1.${dynamic}.class3>");
1014 assert_eq!(open.src, "<div.class1.${dynamic}.class3>");
13881015 }
13891016
13901017 #[test]
......@@ -1392,7 +1019,7 @@ mod tests {
13921019 let result = parse_open("<div.class-${a + b}>");
13931020 assert!(result.is_ok());
13941021 let open = result.unwrap();
1395 assert_eq!(open.content, "<div.class-${a + b}>");
1022 assert_eq!(open.src, "<div.class-${a + b}>");
13961023 }
13971024
13981025 #[test]
......@@ -1400,7 +1027,7 @@ mod tests {
14001027 let result = parse_open("<div.class-$value>");
14011028 assert!(result.is_ok());
14021029 let open = result.unwrap();
1403 assert_eq!(open.content, "<div.class-$value>");
1030 assert_eq!(open.src, "<div.class-$value>");
14041031 }
14051032
14061033 #[test]
......@@ -1408,4 +1035,82 @@ mod tests {
14081035 let result = parse_open("<${MyComponent}/>");
14091036 assert!(result.is_ok());
14101037 }
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 }
14111116}
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 {
3535}
3636
3737impl NodeValue for ErrorBlock {
38 fn render(&self, _: &Node, _: &mut dyn Renderer) {
38 fn render(&self, _: &Node, r: &mut dyn Renderer) {
3939 panic!("cannot render ErrorBlock");
4040 }
4141}
src/plugin/statement.rs+8-5
......@@ -36,17 +36,17 @@ impl BlockRule for Rule {
3636 .iter()
3737 .find(|k| **k == keyword)
3838 .map(|k| k.len())
39 .unwrap_or(0);
39 .unwrap_or(0) as u32;
4040
4141 let unbounded_src = &state.src[state.line_offsets[state.line].first_nonspace..];
4242
4343 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..]) {
4545 Ok(ok) => ok,
4646 Err(err) => {
4747 return Some((
4848 Node::new(ErrorBlock {
49 errors: vec![adjust_err(err, keyword_trim.cast_signed())],
49 errors: vec![adjust_err(err, keyword_trim as isize)],
5050 }),
5151 1,
5252 ));
......@@ -54,8 +54,11 @@ impl BlockRule for Rule {
5454 };
5555
5656 let total_end = keyword_trim + statement_end;
57 let content = &unbounded_src[0..unbounded_src.len().min(total_end + 1)];
58 let line_count = 1 + content[..total_end].chars().filter(|&c| c == '\n').count();
57 let content = &unbounded_src[0..unbounded_src.len().min(total_end as usize + 1)];
58 let line_count = 1 + content[..total_end as usize]
59 .chars()
60 .filter(|&c| c == '\n')
61 .count();
5962
6063 let node = Node::new(StatementBlock {
6164 content: content.into(),
src/plugin/tags.rs+43-66
......@@ -2,21 +2,19 @@ use markdown_it::parser::block::{BlockRule, BlockState};
22use markdown_it::parser::inline::{InlineRoot, InlineRule, InlineState};
33use markdown_it::{Node, NodeValue, Renderer};
44
5use crate::marko::{self, OpenOrClose};
5use crate::marko_ast::OpenOrClose;
66use crate::plugin::{get_line_raw, ErrorBlock};
7use crate::{marko, marko_ast};
78
89/// An opening Marko tag: <div>, <if=cond>, <for|item| of=items>, etc.
910#[derive(Debug)]
1011pub struct MarkoOpen {
11 pub content: String,
12 pub tag_name: String,
13 /// Length of the tag name (for error span highlighting)
14 pub tag_name_len: usize,
12 pub open: marko_ast::OpenOwned,
1513}
1614
1715impl NodeValue for MarkoOpen {
1816 fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {
19 fmt.text_raw(&self.content);
17 fmt.text_raw(&self.open.src);
2018 fmt.text_raw("\n");
2119 }
2220}
......@@ -54,15 +52,12 @@ impl NodeValue for MarkoSelfClosing {
5452/// The text is parsed as inline markdown (children) but not wrapped in <p>
5553#[derive(Debug)]
5654pub struct MarkoOpenWithText {
57 pub open_tag: String,
58 pub tag_name: String,
59 /// Length of the tag name (for error span highlighting)
60 pub tag_name_len: usize,
55 pub open: marko_ast::OpenOwned,
6156}
6257
6358impl NodeValue for MarkoOpenWithText {
6459 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
65 fmt.text_raw(&self.open_tag);
60 fmt.text_raw(&self.open.src);
6661 fmt.text_raw("\n");
6762 fmt.contents(&node.children);
6863 fmt.text_raw("\n");
......@@ -157,37 +152,31 @@ fn find_same_line_close<'a>(content: &'a str, tag_name: &str) -> Option<(&'a str
157152
158153 // Try to parse as a tag
159154 match marko::parse_tag(&content[lt_pos..]) {
160 Ok(marko::OpenOrClose::Open(open)) => {
155 Ok(OpenOrClose::Open(open)) => {
161156 if !open.self_closing {
162 stack.push(open.tag_name);
157 stack.push(open.tag_name());
163158 }
164 pos = lt_pos + open.content.len();
159 pos = lt_pos + (open.src.len());
165160 }
166 Ok(marko::OpenOrClose::Close(close)) => {
161 Ok(OpenOrClose::Close(close)) => {
167162 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 </>
170163 if close.tag_name.map(|n| n == tag_name).unwrap_or(true) {
171164 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];
173166 return Some((content_before, close_tag));
174167 }
175 // Named close tag that doesn't match - keep scanning
176 pos = lt_pos + close.length;
168 pos = lt_pos + close.length as usize;
177169 } else {
178 // Close tag for a nested open tag - pop the stack
179 // Generic </> closes the most recent, named must match
180170 if close.tag_name.is_none() {
181171 stack.pop();
182172 } else if stack.last() == close.tag_name.as_ref() {
183173 stack.pop();
184174 }
185 // If it doesn't match, we still continue (malformed nesting)
186 pos = lt_pos + close.length;
175 pos = lt_pos + close.length as usize;
187176 }
188177 }
189178 Err(_) => {
190 // Not a valid tag, skip past this '<'
179 // TODO: why no forward
191180 pos = lt_pos + 1;
192181 }
193182 }
......@@ -212,13 +201,13 @@ impl BlockRule for Rule {
212201 match marko::parse_tag(unbounded_src) {
213202 Ok(OpenOrClose::Open(open)) => {
214203 // 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
217206 if open.self_closing {
218207 // <input /> - self-closing tag
219208 return Some((
220209 Node::new(MarkoSelfClosing {
221 content: open.content.to_string(),
210 content: open.src.to_string(),
222211 }),
223212 lines_consumed,
224213 ));
......@@ -226,18 +215,19 @@ impl BlockRule for Rule {
226215
227216 // Check if there's content on the same line after the tag
228217 // 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()..];
230219 let rest_of_last_line = rest_after_tag.lines().next().unwrap_or("");
231220 if !rest_of_last_line.trim().is_empty() {
232221 let text = rest_of_last_line.trim();
233222
234223 // 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 {
236226 // Complete tag on one line: <tag>content</tag>
237227 let content = content.trim();
238228
239229 // Calculate byte offset for source mapping
240 let text_start_in_line = open.content.len()
230 let text_start_in_line = open.src.len()
241231 + (rest_of_last_line.len() - rest_of_last_line.trim_start().len());
242232 let line_start = state.line_offsets[state.line].first_nonspace;
243233 let text_start = line_start + text_start_in_line;
......@@ -245,10 +235,10 @@ impl BlockRule for Rule {
245235 let mapping = vec![(0, text_start)];
246236
247237 let mut node = Node::new(MarkoBlockComplete {
248 open_tag: open.content.to_string(),
238 open_tag: open.src.to_string(),
249239 close_tag: close_tag.to_string(),
250 tag_name: open.tag_name.to_string(),
251 tag_name_len: open.tag_name.len(),
240 tag_name: open.tag_name().to_string(),
241 tag_name_len: open.tag_name().len(),
252242 });
253243
254244 if !content.is_empty() {
......@@ -261,7 +251,7 @@ impl BlockRule for Rule {
261251
262252 // No close tag - just content after open tag
263253 // 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()
265255 + (rest_of_last_line.len() - rest_of_last_line.trim_start().len());
266256 let line_start = state.line_offsets[state.line].first_nonspace;
267257 let text_start = line_start + text_start_in_line;
......@@ -269,9 +259,7 @@ impl BlockRule for Rule {
269259 let mapping = vec![(0, text_start)];
270260
271261 let mut node = Node::new(MarkoOpenWithText {
272 open_tag: open.content.to_string(),
273 tag_name: open.tag_name.to_string(),
274 tag_name_len: open.tag_name.len(),
262 open: open.to_owned(),
275263 });
276264 node.children
277265 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));
......@@ -295,9 +283,7 @@ impl BlockRule for Rule {
295283 let mapping = vec![(0, next_line_start)];
296284
297285 let mut node = Node::new(MarkoOpenWithText {
298 open_tag: open.content.to_string(),
299 tag_name: open.tag_name.to_string(),
300 tag_name_len: open.tag_name.len(),
286 open: open.to_owned(),
301287 });
302288 node.children
303289 .push(Node::new(InlineRoot::new(text.to_string(), mapping)));
......@@ -310,9 +296,7 @@ impl BlockRule for Rule {
310296 // Clean case: open tag on its own line(s), followed by blank or another tag
311297 Some((
312298 Node::new(MarkoOpen {
313 content: open.content.to_string(),
314 tag_name: open.tag_name.to_string(),
315 tag_name_len: open.tag_name.len(),
299 open: open.to_owned(),
316300 }),
317301 lines_consumed,
318302 ))
......@@ -320,15 +304,15 @@ impl BlockRule for Rule {
320304 Ok(OpenOrClose::Close(close)) => {
321305 // Close tags should be single line, but check anyway
322306 let lines_consumed = 1 + close.length.saturating_sub(1).min(
323 unbounded_src[..close.length]
307 unbounded_src[..close.length as usize]
324308 .chars()
325309 .filter(|&c| c == '\n')
326 .count(),
310 .count() as u32,
327311 );
328312
329313 // Check if there's content on the same line after the close tag
330 let close_text = &unbounded_src[..close.length];
331 let rest_after_tag = &unbounded_src[close.length..];
314 let close_text = &unbounded_src[..close.length as usize];
315 let rest_after_tag = &unbounded_src[close.length as usize..];
332316 let rest_of_line = rest_after_tag.lines().next().unwrap_or("");
333317 if !rest_of_line.trim().is_empty() {
334318 // TODO: content after close tag on same line
......@@ -341,7 +325,7 @@ impl BlockRule for Rule {
341325 tag_name: close.tag_name.map(|s| s.to_string()),
342326 tag_name_len: close.tag_name.map(|s| s.len()),
343327 }),
344 lines_consumed,
328 lines_consumed as usize,
345329 ))
346330 }
347331 Err(err) => Some((Node::new(ErrorBlock { errors: vec![err] }), 1)),
......@@ -354,50 +338,43 @@ impl InlineRule for Rule {
354338
355339 fn run(state: &mut InlineState) -> Option<(Node, usize)> {
356340 let input = &state.src[state.pos..state.pos_max];
357
358 // Skip if this doesn't look like a Marko tag
359341 if !input.starts_with("<") {
360342 return None;
361343 }
362344
363 // Try to parse as a Marko tag
364345 match marko::parse_tag(input) {
365346 Ok(OpenOrClose::Open(open)) => {
366347 if open.self_closing {
367 // Self-closing inline tag like <br/>
368348 return Some((
369349 Node::new(MarkoSelfClosing {
370 content: open.content.to_string(),
350 content: open.src.to_string(),
371351 }),
372 open.content.len(),
352 open.src.len(),
373353 ));
374354 }
375 // Emit open marker - will be matched by post-processor
355
376356 Some((
377357 Node::new(MarkoInlineOpenMarker {
378 content: open.content.to_string(),
379 tag_name: open.tag_name.to_string(),
380 tag_name_len: open.tag_name.len(),
358 content: open.src.to_string(),
359 tag_name: open.tag_name().to_owned(),
360 tag_name_len: open.tag_name().len(),
381361 }),
382 open.content.len(),
362 open.src.len(),
383363 ))
384364 }
385365 Ok(OpenOrClose::Close(close)) => {
386 // Emit close marker - will be matched by post-processor
387 let close_text = &input[..close.length];
366 let close_text = &input[..close.length as usize];
388367 Some((
389368 Node::new(MarkoInlineCloseMarker {
390369 content: close_text.to_string(),
391370 tag_name: close.tag_name.map(|s| s.to_string()),
392371 tag_name_len: close.tag_name.map(|s| s.len()),
393372 }),
394 close.length,
373 close.length as usize,
395374 ))
396375 }
397 Err(_) => {
398 // Not a valid Marko tag, let other rules handle it
399 None
400 }
376 // TODO: why this no reported
377 Err(_) => None,
401378 }
402379 }
403380}
src/plugin/toc.rs+3-3
......@@ -84,14 +84,14 @@ fn collect_headings_recursive(
8484 }
8585 // Check for MarkoOpen tags that are h1-h6
8686 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()) {
8888 let text = node
8989 .children
9090 .iter()
9191 .map(|c| c.collect_text())
9292 .collect::<Vec<_>>()
9393 .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| {
9595 s.find("id=\"")
9696 .map(|pos| {
9797 let start = pos + 4;
......@@ -113,7 +113,7 @@ fn collect_headings_recursive(
113113 }
114114 // Check for MarkoOpenWithText tags that are h1-h6
115115 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()) {
117117 let text = node
118118 .children
119119 .iter()
src/typescript.rs+66-70
......@@ -1,23 +1,22 @@
1//! implement ts partial parsing helpers
2use crate::{adjust_err, err, marko::LexState};
1//! implements ts partial parsing helpers
2use crate::{adjust_err, err, marko_ast::LexState};
33
44use oxc_allocator::Allocator;
55use oxc_ast::ast::{Expression, Statement};
66use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
77use oxc_span::{GetSpan, SourceType};
88
9/// TODO: Replace with oxc parser, but oxc is tough here
10pub fn scan_first_statement_forbid_trailing(source: &str) -> Result<usize, OxcDiagnostic> {
9pub fn scan_first_statement_forbid_trailing(source: &str) -> Result<u32, OxcDiagnostic> {
1110 let mut allocator = Allocator::new();
1211 let expr = parse_stmt_extra(source, 0, &mut allocator)?;
1312 let span = expr.span();
14 let len = (span.end - span.start) as usize;
13 let len = span.end - span.start;
1514
1615 if let Some(trailing) = source[span.end as usize..].lines().next() {
1716 if trailing.trim().len() > 0 {
1817 return Err(err(
1918 "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,
2120 trailing.trim().len(),
2221 ));
2322 }
......@@ -33,7 +32,7 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
3332 if source.is_empty() {
3433 return Err(err(
3534 "Expected expression, found end of file",
36 offset.max(0).cast_unsigned(),
35 offset.max(0).cast_unsigned() as u32,
3736 1,
3837 ));
3938 }
......@@ -52,7 +51,13 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
5251 let first_err_offset = first_err
5352 .labels
5453 .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 })?
5661 .first()
5762 .expect("labels, but no labels!")
5863 .offset();
......@@ -82,17 +87,18 @@ fn parse_stmt_extra<'alloc, 'src: 'alloc>(
8287 assert!(!result.panicked);
8388 }
8489
85 result
86 .program
87 .body
88 .into_iter()
89 .next()
90 .ok_or_else(|| err("Expected statement", offset.max(0).cast_unsigned(), 1))
90 result.program.body.into_iter().next().ok_or_else(|| {
91 err(
92 "Expected statement",
93 offset.max(0).cast_unsigned() as u32,
94 1,
95 )
96 })
9197}
9298
9399fn parse_expr_extra<'alloc, 'src: 'alloc>(
94100 source: &'src str,
95 offset: isize,
101 offset: i32,
96102 allocator: &'alloc mut Allocator,
97103) -> Result<Expression<'alloc>, OxcDiagnostic> {
98104 if source.is_empty() {
......@@ -130,7 +136,7 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(
130136 let mut whitespace_groups = 0;
131137 loop {
132138 if candidate.is_empty() {
133 return Err(adjust_err(first_err, offset));
139 return Err(adjust_err(first_err, offset as isize));
134140 }
135141 match oxc_parser::Parser::new(allocator, candidate, source_type).parse_expression()
136142 {
......@@ -141,7 +147,7 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(
141147 if after_trim.len() < before_trim.len() {
142148 whitespace_groups += 1;
143149 if whitespace_groups > 1 {
144 return Err(adjust_err(first_err, offset));
150 return Err(adjust_err(first_err, offset as isize));
145151 }
146152 }
147153 candidate = after_trim;
......@@ -164,17 +170,16 @@ fn parse_expr_extra<'alloc, 'src: 'alloc>(
164170}
165171
166172/// 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> {
168174 let mut allocator = Allocator::new();
169175 let expr = parse_expr_extra(l.peek_rest(), l.offset().cast_signed(), &mut allocator)?;
170176 let span = expr.span();
171 let len = (span.end - span.start) as usize;
172 l.advance(len);
173 Ok(len)
177 l.advance(span.end - span.start);
178 Ok(span.end - span.start)
174179}
175180
176181/// 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> {
178183 use oxc_ast::ast::{BinaryOperator, Expression};
179184
180185 let rest = l.peek_rest();
......@@ -196,7 +201,7 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
196201 }
197202 }
198203
199 let length = e.span().end as usize;
204 let length = e.span().end;
200205 l.advance(length);
201206 return Ok(length);
202207 }
......@@ -215,9 +220,9 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
215220 allocator.reset();
216221 if let Ok(expr) = parse_expr_extra(candidate, l.offset().cast_signed(), &mut allocator) {
217222 // 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;
219224 // 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) {
221226 l.advance(length);
222227 return Ok(length);
223228 }
......@@ -227,18 +232,16 @@ pub fn parse_expr_without_gt(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
227232 // Last resort: return error from the original parse attempt
228233 allocator.reset();
229234 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;
231236 l.advance(length);
232237 Ok(length)
233238}
234239
235240/// 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> {
237242 let source = l.peek_rest();
238243 if !source.starts_with("(") {
239 return Err(
240 OxcDiagnostic::error("Expected `(`").and_label(LabeledSpan::new(None, l.offset(), 1)),
241 );
244 return Err(err("Expected `(`", l.offset(), 1));
242245 }
243246
244247 // 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> {
248251 let expr = parse_expr_extra(wrapped.as_str(), l.offset().cast_signed(), &mut allocator)?;
249252
250253 // Walk down the left side of the AST to find the CallExpression
251 let call_span = find_leftmost_call(&expr).ok_or_else(|| {
252 OxcDiagnostic::error("Expected call expression").and_label(LabeledSpan::new(
253 None,
254 l.offset(),
255 1,
256 ))
257 })?;
254 let call_span =
255 find_leftmost_call(&expr).ok_or_else(|| err("Expected call expression", l.offset(), 1))?;
258256
259257 // Subtract the `f` prefix we added
260 let length = call_span.end as usize - 1;
258 let length = call_span.end - 1;
261259 l.advance(length);
262260 Ok(length)
263261}
264262
265263/// parse variable binding. identifier or destructuring pattern
266264/// 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> {
268266 // TODO: this approach has bugs
269267 let offset = l.offset();
270268 let source = l.peek_rest();
......@@ -276,16 +274,16 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
276274 let end = {
277275 let expr = parse_expr_extra(source, l.offset().cast_signed(), &mut allocator)?;
278276 let span = find_leftmost(&expr, LeftmostSearch::Assignment).unwrap();
279 span.end as usize
277 span.end
280278 };
281279 allocator.reset();
282280
283281 {
284282 let prefix = "function f(";
285 let source = format!("{prefix}{}) {{}}", &source[0..end]);
283 let source = format!("{prefix}{}) {{}}", &source[0..end as usize]);
286284 parse_expr_extra(
287285 source.as_str(),
288 l.offset().cast_signed() - prefix.len().cast_signed(),
286 l.offset().cast_signed() - (prefix.len().cast_signed() as i32),
289287 &mut allocator,
290288 )?;
291289 }
......@@ -305,7 +303,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
305303}
306304
307305/// parses a type
308pub fn parse_type(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
306pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
309307 let mut allocator = Allocator::default();
310308 let source = format!("T as {}", l.peek_rest());
311309 println!("{{{source}}} HUH");
......@@ -319,19 +317,16 @@ pub fn parse_type(l: &mut LexState) -> Result<usize, OxcDiagnostic> {
319317 expr.span().end as usize,
320318 )
321319 })?;
322 let len = (span.end - span.start) as usize;
323
320 let len = span.end - span.start;
324321 l.advance(len);
325322 Ok(len)
326323}
327324
328325/// 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> {
330327 let source = l.peek_rest();
331328 if !source.starts_with("(") {
332 return Err(
333 OxcDiagnostic::error("Expected `(`").and_label(LabeledSpan::new(None, l.offset(), 1)),
334 );
329 return Err(err("Expected `(`", l.offset(), 1));
335330 }
336331
337332 // 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
340335
341336 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
345340 // Find the function expression (walk left side of any binary/etc expressions)
346 let func_span = find_leftmost(&expr, LeftmostSearch::Function).ok_or_else(|| {
347 OxcDiagnostic::error("Expected function expression").and_label(LabeledSpan::new(
348 None,
349 l.offset(),
350 1,
351 ))
352 })?;
341 let func_span = find_leftmost(&expr, LeftmostSearch::Function)
342 .ok_or_else(|| err("Expected function expression", l.offset(), 1))?;
353343
354344 // 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);
356346 l.advance(length);
357347 Ok(length)
358348}
......@@ -483,7 +473,7 @@ fn find_leftmost_assign(
483473mod tests {
484474 use super::*;
485475
486 fn parse_expr(source: &str) -> Result<usize, OxcDiagnostic> {
476 fn parse_expr(source: &str) -> Result<u32, OxcDiagnostic> {
487477 let mut l = LexState::new(source);
488478 super::parse_expr(&mut l)
489479 }
......@@ -568,7 +558,7 @@ mod tests {
568558 assert!(result.is_err());
569559 }
570560
571 fn parse_expr_no_gt(source: &str) -> Result<usize, OxcDiagnostic> {
561 fn parse_expr_no_gt(source: &str) -> Result<u32, OxcDiagnostic> {
572562 let mut l = LexState::new(source);
573563 parse_expr_without_gt(&mut l)
574564 }
......@@ -621,7 +611,7 @@ mod tests {
621611 assert_eq!(parse_expr_no_gt("x >>> 2"), Ok(1));
622612 }
623613
624 fn parse_call_args(source: &str) -> Result<usize, OxcDiagnostic> {
614 fn parse_call_args(source: &str) -> Result<u32, OxcDiagnostic> {
625615 let mut l = LexState::new(source);
626616 parse_call_arguments(&mut l)
627617 }
......@@ -671,7 +661,7 @@ mod tests {
671661 assert!(result.is_err());
672662 }
673663
674 fn parse_fn_params_body(source: &str) -> Result<usize, OxcDiagnostic> {
664 fn parse_fn_params_body(source: &str) -> Result<u32, OxcDiagnostic> {
675665 let mut l = LexState::new(source);
676666 parse_fn_params_and_body(&mut l)
677667 }
......@@ -736,7 +726,7 @@ mod tests {
736726 assert!(result.is_err());
737727 }
738728
739 fn parse_var_binding(source: &str) -> Result<usize, OxcDiagnostic> {
729 fn parse_var_binding(source: &str) -> Result<u32, OxcDiagnostic> {
740730 let mut l = LexState::new(source);
741731 super::parse_var_binding(&mut l)
742732 }
......@@ -816,35 +806,41 @@ mod tests {
816806 fn test_stmt_function_with_garbage() {
817807 let source = "function hello() {\n console.log(1);\n}\n\nrandom markdown garbage";
818808 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 );
820813 }
821814
822815 #[test]
823816 fn test_stmt_import_with_garbage() {
824817 let source = "import { foo } from 'bar';\n\n# markdown heading";
825818 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';");
827820 }
828821
829822 #[test]
830823 fn test_stmt_interface_with_garbage() {
831824 let source = "interface Foo {\n bar: string;\n}\n\nsome text";
832825 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 );
834830 }
835831
836832 #[test]
837833 fn test_stmt_const_declaration() {
838834 let source = "const answer = 42;\n\n# Next section";
839835 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;");
841837 }
842838
843839 #[test]
844840 fn test_stmt_expression_statement() {
845841 let source = "console.log('hello');\n\nmore content";
846842 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');");
848844 }
849845
850846 #[test]
......@@ -858,8 +854,8 @@ mod tests {
858854 fn test_stmt_multiline_function() {
859855 let source = "function sort(items) {\n while (!isSorted()) {\n shuffle(items);\n }\n return items;\n}\n\n# Heading";
860856 let end = scan_first_statement_forbid_trailing(source).unwrap();
861 assert!(source[..end].contains("return items;"));
862 assert!(source[..end].contains("}"));
857 assert!(source[..end as usize].contains("return items;"));
858 assert!(source[..end as usize].contains("}"));
863859 }
864860
865861 #[test]
tests/fixtures/23-outline-extracting.marko+3
......@@ -7,6 +7,9 @@ good night
77<define/Header_3__markodown__>
88<strong>snow</strong> time
99</>
10rain <strong>time</strong>
11<define/Header_4__markodown__>
12</>
1013<Layout__markodown__ module=self__markodown__ outline=[
1114 { level: 1, id: 'good-morning', content: Header_1__markodown__ },
1215 { level: 2, id: 'good-night', content: Header_2__markodown__ },