1use pest::Parser;
2use pest::error::ErrorVariant;
3use pest::iterators::Pair;
4
5#[derive(pest_derive::Parser)]
6#[grammar = "grammar.pest"]
7struct SfcParser;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct Attr {
11 pub name: String,
12 pub value: Option<String>, }
14
15#[derive(Debug, Clone, PartialEq, Default)]
16pub struct TemplateBlock {
17 pub attrs: Vec<Attr>,
18 pub content: String,
19}
20
21#[derive(Debug, Clone, PartialEq, Default)]
22pub struct ScriptBlock {
23 pub attrs: Vec<Attr>,
24 pub content: String,
25 pub setup: bool,
26}
27
28#[derive(Debug, Clone, PartialEq, Default)]
29pub struct StyleBlock {
30 pub attrs: Vec<Attr>,
31 pub content: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Default)]
35pub struct Sfc {
36 pub template: Option<TemplateBlock>,
37 pub script_setup: Option<ScriptBlock>,
38 pub script: Option<ScriptBlock>,
39 pub style: Option<StyleBlock>,
40 pub warnings: Vec<String>,
51}
52
53fn format_pest_error(err: pest::error::Error<Rule>, source: &str) -> String {
57 let (line, column) = match err.line_col {
60 pest::error::LineColLocation::Pos((l, c)) => (l, c),
61 pest::error::LineColLocation::Span((l, c), _) => (l, c),
62 };
63
64 let mut expected: Vec<String> = Vec::new();
67 let mut unexpected: Vec<String> = Vec::new();
68 let description = match &err.variant {
69 ErrorVariant::ParsingError {
70 positives,
71 negatives,
72 } => {
73 expected = positives.iter().map(rule_to_block_name).collect::<Vec<_>>();
74 unexpected = negatives.iter().map(rule_to_block_name).collect::<Vec<_>>();
75
76 if expected.is_empty() {
77 "unexpected token found while parsing the SFC".to_string()
78 } else {
79 let unexpected_str = if unexpected.is_empty() {
80 "an unexpected token".to_string()
81 } else {
82 unexpected.join(", ")
83 };
84 format!(
85 "expected {} but found {}",
86 expected.join(", "),
87 unexpected_str
88 )
89 }
90 }
91 ErrorVariant::CustomError { message } => message.clone(),
92 };
93
94 let block_context = infer_block_context(source, line);
97 let message = format!("{description} while parsing the {block_context} block");
98
99 let suggestion = suggest_pest_error(&expected, &unexpected, &block_context);
101
102 crate::diagnostic::render_parse_error(source, line, column, 1, &message, suggestion.as_deref())
103}
104
105fn suggest_pest_error(expected: &[String], unexpected: &[String], block: &str) -> Option<String> {
108 let end_of_input = expected
109 .iter()
110 .any(|e| e.contains("end of input") || e.contains("SFC file"));
111 let unexp_join = unexpected.join(" ");
112
113 if end_of_input {
114 return Some(format!(
115 "the {block} block may be missing its closing tag (e.g. </{block}>), or a block was never opened"
116 ));
117 }
118
119 if block != "top-level" {
120 return Some(format!(
122 "check the {block} tags and attribute quotes near this line; \
123 every value after '=' should be wrapped in \"double\" or 'single' quotes"
124 ));
125 }
126
127 if unexp_join.contains("end of input") {
128 return Some(
129 "the file ended before a block was closed — add the missing </template>, </script>, or </style>".to_string(),
130 );
131 }
132
133 if expected.iter().any(|e| e.contains("<template")) {
134 return Some(
135 "after a <template> block you need a <script setup> block to define component state and logic"
136 .to_string(),
137 );
138 }
139
140 None
141}
142
143fn rule_to_block_name(rule: &Rule) -> String {
145 match rule {
146 Rule::template | Rule::template_open | Rule::template_body | Rule::nested_template => {
147 "<template>".to_string()
148 }
149 Rule::slot_attr => "slot binding (v-slot:name or #name)".to_string(),
153 Rule::slot_name => "slot name".to_string(),
154 Rule::script | Rule::script_open | Rule::script_body => "<script>".to_string(),
155 Rule::style | Rule::style_open | Rule::style_body => "<style>".to_string(),
156 Rule::attribute => "attribute".to_string(),
157 Rule::ident => "identifier".to_string(),
158 Rule::quoted | Rule::dq | Rule::sq => "quoted value".to_string(),
159 Rule::block => "SFC block (<template>, <script>, or <style>)".to_string(),
160 Rule::file => "SFC file".to_string(),
161 Rule::WS => "whitespace".to_string(),
162 Rule::EOI => "end of input".to_string(),
163 }
164}
165
166fn infer_block_context(source: &str, error_line: usize) -> String {
170 let lines: Vec<&str> = source.lines().collect();
171 let mut last_block = "top-level";
172
173 for (idx, line) in lines.iter().enumerate() {
174 let ln = idx + 1;
175 if ln > error_line {
176 break;
177 }
178 let trimmed = line.trim();
179 if trimmed.starts_with("<template") {
180 last_block = "template";
181 } else if trimmed.starts_with("<script") {
182 last_block = "script";
183 } else if trimmed.starts_with("<style") {
184 last_block = "style";
185 } else if trimmed.starts_with("</template")
186 || trimmed.starts_with("</script")
187 || trimmed.starts_with("</style")
188 {
189 last_block = "top-level";
190 }
191 }
192
193 last_block.to_string()
194}
195
196pub const MAX_NESTED_TEMPLATE_DEPTH: usize = 256;
237
238fn nested_template_openers(source: &str) -> (usize, usize) {
246 const OPEN: &str = "<template";
247 const WS: [char; 4] = [' ', '\t', '\r', '\n'];
256 let mut count = 0usize;
257 let mut offending_offset = 0usize;
258 for (i, _) in source.char_indices() {
264 if !source[i..].starts_with(OPEN) {
265 continue;
266 }
267 let opens_a_block = source[i + OPEN.len()..]
268 .chars()
269 .next()
270 .is_some_and(|after| after == '>' || WS.contains(&after));
271 if !opens_a_block {
272 continue;
273 }
274 if count == MAX_NESTED_TEMPLATE_DEPTH {
279 offending_offset = i;
280 }
281 count += 1;
282 }
283 (count, offending_offset)
284}
285
286fn check_nested_template_depth(source: &str) -> Result<(), String> {
289 let (openers, offending_offset) = nested_template_openers(source);
290 if openers <= MAX_NESTED_TEMPLATE_DEPTH {
291 return Ok(());
292 }
293 let (line, col) = crate::diagnostic::line_col_at(source, offending_offset);
294 Err(crate::diagnostic::render_parse_error(
295 source,
296 line,
297 col,
298 "<template".len(),
299 &format!(
300 "{openers} nested `<template` blocks exceed the limit of \
301 {MAX_NESTED_TEMPLATE_DEPTH}"
302 ),
303 Some(
304 "nest fewer than 256 `<template` blocks deep, or flatten the markup — \
305 a `<template v-slot:name>` block is the only thing that nests, so this \
306 means a slot-fragment tree far deeper than any real template",
307 ),
308 ))
309}
310
311pub fn parse_sfc(source: &str) -> Result<Sfc, String> {
312 let mut sfc = Sfc::default();
313
314 check_nested_template_depth(source)?;
319
320 let mut pairs =
322 SfcParser::parse(Rule::file, source).map_err(|e| format_pest_error(e, source))?;
323 let file = pairs
324 .next()
325 .ok_or_else(|| "SFC parse error: input is empty".to_string())?;
326 debug_assert!(file.as_rule() == Rule::file);
327
328 for node in file.into_inner() {
331 match node.as_rule() {
332 Rule::block => {
333 for inner in node.into_inner() {
334 consume_top_level(inner, &mut sfc);
335 }
336 }
337 Rule::template | Rule::script | Rule::style => {
339 consume_top_level(node, &mut sfc);
340 }
341 _ => {}
342 }
343 }
344
345 sfc.warnings = template_warnings(&sfc);
346
347 Ok(sfc)
348}
349
350fn template_warnings(sfc: &Sfc) -> Vec<String> {
367 let Some(template) = &sfc.template else {
368 return Vec::new();
369 };
370 let mut known: Vec<String> = Vec::new();
371 if let Some(script_setup) = &sfc.script_setup {
372 let mut resolver =
373 crate::component_resolver::ComponentResolver::new(std::path::PathBuf::from("."));
374 resolver.parse_imports(&script_setup.content);
375 known.extend(resolver.component_names());
376 }
377 let known_refs: Vec<&str> = known.iter().map(String::as_str).collect();
378 match crate::template_parse::parse_template(&template.content, &known_refs) {
379 Ok(diag) => diag.warnings,
380 Err(_) => Vec::new(),
381 }
382}
383
384fn consume_top_level(node: Pair<Rule>, sfc: &mut Sfc) {
385 match node.as_rule() {
386 Rule::template => {
387 let (attrs, content) = parse_template(node);
388 sfc.template = Some(TemplateBlock { attrs, content });
389 }
390 Rule::script => {
391 let (attrs, content) = parse_script(node);
392 let setup = has_bool_attr(&attrs, "setup");
393 let sb = ScriptBlock {
394 attrs,
395 content,
396 setup,
397 };
398 if setup {
399 sfc.script_setup = Some(sb);
400 } else {
401 sfc.script = Some(sb);
402 }
403 }
404 Rule::style => {
405 let (attrs, content) = parse_style(node);
406 sfc.style = Some(StyleBlock { attrs, content });
407 }
408 _ => {}
409 }
410}
411
412fn parse_template(tpl: Pair<Rule>) -> (Vec<Attr>, String) {
413 let mut attrs = Vec::new();
414 let mut content = String::new();
415
416 for p in tpl.into_inner() {
417 match p.as_rule() {
418 Rule::template_open => {
419 for a in p.into_inner() {
421 if a.as_rule() == Rule::attribute {
422 attrs.push(parse_attr(a));
423 }
424 }
425 }
426 Rule::template_body => content = p.as_str().to_string(),
427 _ => {}
428 }
429 }
430 (attrs, content)
431}
432
433fn parse_script(scr: Pair<Rule>) -> (Vec<Attr>, String) {
434 let mut attrs = Vec::new();
435 let mut content = String::new();
436
437 for p in scr.into_inner() {
438 match p.as_rule() {
439 Rule::script_open => {
440 for a in p.into_inner() {
441 if a.as_rule() == Rule::attribute {
442 attrs.push(parse_attr(a));
443 }
444 }
445 }
446 Rule::script_body => content = p.as_str().to_string(),
447 _ => {}
448 }
449 }
450 (attrs, content)
451}
452
453fn parse_style(sty: Pair<Rule>) -> (Vec<Attr>, String) {
454 let mut attrs = Vec::new();
455 let mut content = String::new();
456
457 for p in sty.into_inner() {
458 match p.as_rule() {
459 Rule::style_open => {
460 for a in p.into_inner() {
461 if a.as_rule() == Rule::attribute {
462 attrs.push(parse_attr(a));
463 }
464 }
465 }
466 Rule::style_body => content = p.as_str().to_string(),
467 _ => {}
468 }
469 }
470 (attrs, content)
471}
472
473fn parse_attr(attr: Pair<Rule>) -> Attr {
474 let mut name = String::new();
476 let mut value: Option<String> = None;
477
478 for part in attr.into_inner() {
479 match part.as_rule() {
480 Rule::ident => name = part.as_str().to_string(),
481 Rule::quoted => value = Some(strip_quotes(part.as_str())),
482 _ => {}
483 }
484 }
485 Attr { name, value }
486}
487
488fn strip_quotes(s: &str) -> String {
489 let b = s.as_bytes();
490 if b.len() >= 2
491 && ((b[0] == b'"' && b[b.len() - 1] == b'"') || (b[0] == b'\'' && b[b.len() - 1] == b'\''))
492 {
493 s[1..s.len() - 1].to_string()
494 } else {
495 s.to_string()
496 }
497}
498
499fn has_bool_attr(attrs: &[Attr], key: &str) -> bool {
500 attrs.iter().any(|a| a.name == key)
501}
502
503pub fn validate_sfc(sfc: &Sfc) -> Vec<String> {
510 let mut errors: Vec<String> = Vec::new();
511
512 if sfc.template.is_some() && sfc.script_setup.is_none() && sfc.script.is_none() {
515 errors.push(
516 "SFC has a <template> block but no <script> or <script setup> block — \
517 the component will lack state and event handling"
518 .to_string(),
519 );
520 }
521
522 errors
523}