vue_compiler_core/
parser.rs

1// Vue Template Parser does not adhere to HTML spec.
2// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction
3// According to the spec: tree construction has several points:
4// 1. Tree Construction Dispatcher: N/A. We don't consider foreign content.
5// 2. appropriate place for inserting a node: For table/template elements.
6//    N/A.  We can't know the global tree in a component.
7// 3. create an element for a token: For custom component
8//    N/A. We don't handle JS execution for custom component.
9// 4. adjust MathML/SVG attributes:
10//    ?? Should we handle this? The original Vue compiler does not.
11// 5. Inserting Text/Comment: N/A. We don't handle script/insertion location.
12// 6. Parsing elements that contain only text: Already handled in scanner.
13// 7. Closing elements that have implied end tags:
14//    N/A: Rule is too complicated and requires non-local context.
15// Instead, we use a simple stack to construct AST.
16
17use super::{
18    error::{CompilationError, CompilationErrorKind as ErrorKind, ErrorHandler},
19    scanner::{Attribute, AttributeValue, Tag, TextMode, Token, TokenSource},
20    util::{find_dir, is_core_component, no, non_whitespace, yes, VStr},
21    Name, Namespace, SourceLocation,
22};
23#[cfg(feature = "serde")]
24use serde::Serialize;
25use smallvec::{smallvec, SmallVec};
26use std::ops::Deref;
27
28#[cfg_attr(feature = "serde", derive(Serialize))]
29pub enum AstNode<'a> {
30    Element(Element<'a>),
31    Text(TextNode<'a>),
32    Interpolation(SourceNode<'a>),
33    Comment(SourceNode<'a>),
34}
35
36impl<'a> AstNode<'a> {
37    pub fn get_element(&self) -> Option<&Element<'a>> {
38        match self {
39            AstNode::Element(e) => Some(e),
40            _ => None,
41        }
42    }
43    pub fn get_element_mut(&mut self) -> Option<&mut Element<'a>> {
44        match self {
45            AstNode::Element(e) => Some(e),
46            _ => None,
47        }
48    }
49    pub fn into_element(self) -> Element<'a> {
50        match self {
51            AstNode::Element(e) => e,
52            _ => panic!("call into_element on non-element AstNode"),
53        }
54    }
55    pub fn get_location(&self) -> &SourceLocation {
56        match self {
57            Self::Element(e) => &e.location,
58            Self::Text(t) => &t.location,
59            Self::Interpolation(i) => &i.location,
60            Self::Comment(c) => &c.location,
61        }
62    }
63}
64
65#[cfg_attr(feature = "serde", derive(Serialize))]
66pub struct SourceNode<'a> {
67    pub source: &'a str,
68    pub location: SourceLocation,
69}
70
71pub struct TextNode<'a> {
72    pub text: SmallVec<[VStr<'a>; 1]>,
73    pub location: SourceLocation,
74}
75#[cfg(feature = "serde")]
76impl<'a> Serialize for TextNode<'a> {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where
79        S: serde::Serializer,
80    {
81        use serde::ser::SerializeStruct;
82        let mut state = serializer.serialize_struct("TextNode", 2)?;
83        let s = self.text.iter().map(|&s| s.into_string());
84        let s: String = s.collect();
85        state.serialize_field("text", &s)?;
86        state.serialize_field("location", &self.location)?;
87        state.end()
88    }
89}
90
91impl<'a> Deref for TextNode<'a> {
92    type Target = str;
93    fn deref(&self) -> &Self::Target {
94        debug_assert!(self.text.len() == 1);
95        &self.text[0]
96    }
97}
98
99impl<'a> TextNode<'a> {
100    pub fn is_all_whitespace(&self) -> bool {
101        self.text.iter().all(|s| !s.chars().any(non_whitespace))
102    }
103    pub fn trim_leading_newline(&mut self) {
104        if self.text.is_empty() {
105            return;
106        }
107        let first = &self.text[0];
108        let offset = if first.starts_with('\n') {
109            1
110        } else if first.starts_with("\r\n") {
111            2
112        } else {
113            return;
114        };
115        if first.len() > offset {
116            self.text[0] = VStr {
117                raw: &first.raw[offset..],
118                ops: first.ops,
119            };
120        } else {
121            self.text.remove(0);
122        }
123    }
124}
125
126#[cfg_attr(feature = "serde", derive(Serialize))]
127pub enum ElemProp<'a> {
128    Attr(Attribute<'a>),
129    Dir(Directive<'a>),
130}
131
132#[derive(PartialEq, Eq)]
133#[cfg_attr(feature = "serde", derive(Serialize))]
134pub enum ElementType {
135    Plain,
136    Component,
137    Template,
138    SlotOutlet,
139}
140
141#[cfg_attr(feature = "serde", derive(Serialize))]
142pub struct Element<'a> {
143    pub tag_name: Name<'a>,
144    pub tag_type: ElementType,
145    pub namespace: Namespace,
146    pub properties: Vec<ElemProp<'a>>,
147    pub children: Vec<AstNode<'a>>,
148    pub location: SourceLocation,
149}
150
151impl<'a> Element<'a> {
152    #[inline]
153    pub fn is_component(&self) -> bool {
154        self.tag_type == ElementType::Component
155    }
156}
157
158/// Directive supports two forms
159/// static and dynamic
160#[cfg_attr(feature = "serde", derive(Serialize))]
161pub enum DirectiveArg<'a> {
162    // :static="val"
163    Static(Name<'a>),
164    Dynamic(Name<'a>), // :[dynamic]="val"
165}
166
167/// Directive has the form
168/// v-name:arg.mod1.mod2="expr"
169#[derive(Default)]
170#[cfg_attr(feature = "serde", derive(Serialize))]
171pub struct Directive<'a> {
172    pub name: &'a str,
173    pub argument: Option<DirectiveArg<'a>>,
174    pub modifiers: Vec<&'a str>,
175    pub expression: Option<AttributeValue<'a>>,
176    pub head_loc: SourceLocation,
177    pub location: SourceLocation,
178}
179
180impl<'a> Directive<'a> {
181    pub fn has_empty_expr(&self) -> bool {
182        self.expression
183            .as_ref()
184            .map_or(true, |v| !v.content.contains(non_whitespace))
185    }
186    pub fn check_empty_expr(&self, kind: ErrorKind) -> Option<CompilationError> {
187        if !self.has_empty_expr() {
188            return None;
189        }
190        let loc = self
191            .expression
192            .as_ref()
193            .map_or(self.head_loc.clone(), |v| v.location.clone());
194        Some(CompilationError::new(kind).with_location(loc))
195    }
196}
197
198#[cfg_attr(feature = "serde", derive(Serialize))]
199pub struct AstRoot<'a> {
200    pub children: Vec<AstNode<'a>>,
201    pub location: SourceLocation,
202}
203
204#[derive(Clone)]
205pub enum WhitespaceStrategy {
206    Preserve,
207    Condense,
208}
209impl Default for WhitespaceStrategy {
210    fn default() -> Self {
211        WhitespaceStrategy::Condense
212    }
213}
214
215// `is_xxx` methods in ParseOption targets different audience.
216// Please refer to project README for more details.
217#[derive(Clone)]
218pub struct ParseOption {
219    pub whitespace: WhitespaceStrategy,
220    pub preserve_comment: bool,
221    pub get_namespace: fn(&str, &Vec<Element<'_>>) -> Namespace,
222    pub get_text_mode: fn(&str) -> TextMode,
223    /// Returns if a tag is self closing.
224    pub is_void_tag: fn(&str) -> bool,
225    // probably we don't need configure pre tag?
226    // in original Vue this is only used for parsing SFC.
227    pub is_pre_tag: fn(&str) -> bool,
228    /// Exposed to end user for customization like importing web-component from React.
229    pub is_custom_element: fn(&str) -> bool,
230    /// For platform developers. Registers platform specific components written in JS.
231    /// e.g. transition, transition-group. Components that require code in Vue runtime.
232    pub is_builtin_component: fn(&str) -> bool,
233    /// For platform developer. Registers platform components written in host language like C++.
234    pub is_native_element: fn(&str) -> bool,
235}
236
237impl Default for ParseOption {
238    fn default() -> Self {
239        Self {
240            whitespace: WhitespaceStrategy::Condense,
241            preserve_comment: true,
242            get_namespace: |_, _| Namespace::Html,
243            get_text_mode: |_| TextMode::Data,
244            is_void_tag: no,
245            is_pre_tag: |s| s == "pre",
246            is_custom_element: no,
247            is_builtin_component: no,
248            is_native_element: yes,
249        }
250    }
251}
252
253pub struct Parser {
254    option: ParseOption,
255}
256
257impl Parser {
258    pub fn new(option: ParseOption) -> Self {
259        Self { option }
260    }
261
262    pub fn parse<'a, Ts, E>(&self, tokens: Ts, err_handle: E) -> AstRoot<'a>
263    where
264        Ts: TokenSource<'a>,
265        E: ErrorHandler,
266    {
267        let need_flag_namespace = tokens.need_flag_hint();
268        AstBuilder {
269            tokens,
270            err_handle,
271            option: self.option.clone(),
272            open_elems: vec![],
273            root_nodes: vec![],
274            pre_count: 0,
275            v_pre_index: None,
276            need_flag_namespace,
277        }
278        .build_ast()
279    }
280}
281
282// TODO: remove Eh as generic
283struct AstBuilder<'a, Ts, Eh>
284where
285    Ts: TokenSource<'a>,
286    Eh: ErrorHandler,
287{
288    tokens: Ts,
289    err_handle: Eh,
290    option: ParseOption,
291    open_elems: Vec<Element<'a>>,
292    root_nodes: Vec<AstNode<'a>>,
293    // how many <pre> already met
294    pre_count: usize,
295    // the idx of v-pre boundary in open_elems
296    // NB: idx is enough since v-pre does not nest
297    v_pre_index: Option<usize>,
298    need_flag_namespace: bool,
299}
300
301// utility method
302impl<'a, Ts, Eh> AstBuilder<'a, Ts, Eh>
303where
304    Ts: TokenSource<'a>,
305    Eh: ErrorHandler,
306{
307    // Insert node into current insertion point.
308    // It's the last open element's children if open_elems is not empty.
309    // Otherwise it is root_nodes.
310    fn insert_node(&mut self, node: AstNode<'a>) {
311        if let Some(elem) = self.open_elems.last_mut() {
312            elem.children.push(node);
313        } else {
314            self.root_nodes.push(node);
315        }
316    }
317
318    fn emit_error(&self, kind: ErrorKind, loc: SourceLocation) {
319        let error = CompilationError::new(kind).with_location(loc);
320        self.err_handle.on_error(error)
321    }
322}
323
324// parse logic
325impl<'a, Ts, Eh> AstBuilder<'a, Ts, Eh>
326where
327    Ts: TokenSource<'a>,
328    Eh: ErrorHandler,
329{
330    fn build_ast(mut self) -> AstRoot<'a> {
331        let start = self.tokens.current_position();
332        while let Some(token) = self.tokens.next() {
333            self.parse_token(token);
334        }
335        self.report_unclosed_script_comment();
336        for _ in 0..self.open_elems.len() {
337            self.close_element(/*has_matched_end*/ false);
338        }
339        debug_assert_eq!(self.pre_count, 0);
340        debug_assert!(self.v_pre_index.is_none());
341        let need_condense = self.need_condense();
342        compress_whitespaces(&mut self.root_nodes, need_condense);
343        let location = self.tokens.get_location_from(start);
344        AstRoot {
345            children: self.root_nodes,
346            location,
347        }
348    }
349
350    fn parse_token(&mut self, token: Token<'a>) {
351        // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody:current-node-26
352        match token {
353            Token::EndTag(s) => self.parse_end_tag(s),
354            Token::Text(text) => self.parse_text(text),
355            Token::StartTag(tag) => self.parse_open_tag(tag),
356            Token::Comment(c) => self.parse_comment(c),
357            Token::Interpolation(i) => self.parse_interpolation(i),
358        };
359    }
360    fn parse_open_tag(&mut self, tag: Tag<'a>) {
361        let Tag {
362            name,
363            self_closing,
364            attributes,
365        } = tag;
366        let props = self.parse_attributes(attributes);
367        let ns = (self.option.get_namespace)(name, &self.open_elems);
368        let elem = Element {
369            tag_name: name,
370            tag_type: ElementType::Plain,
371            namespace: ns,
372            properties: props,
373            children: vec![],
374            location: SourceLocation {
375                start: self.tokens.last_position(),
376                end: self.tokens.current_position(),
377            },
378        };
379        if self_closing || (self.option.is_void_tag)(name) {
380            let node = self.parse_element(elem);
381            self.insert_node(node);
382        } else {
383            // only element with childen needs set pre/v-pre.
384            // self-closing element cancels out pre itself.
385            self.handle_pre_like(&elem);
386            self.open_elems.push(elem);
387            self.set_scanner_flag();
388        }
389    }
390    fn parse_attributes(&mut self, mut attrs: Vec<Attribute<'a>>) -> Vec<ElemProp<'a>> {
391        // in v-pre, parse no directive
392        if self.v_pre_index.is_some() {
393            return attrs.into_iter().map(ElemProp::Attr).collect();
394        }
395        let mut dir_parser = DirectiveParser::new(&self.err_handle);
396        // v-pre precedes any other directives
397        for i in 0..attrs.len() {
398            if attrs[i].name != "v-pre" {
399                continue;
400            }
401            let dir = dir_parser.parse(attrs.remove(i));
402            let mut ret = vec![ElemProp::Dir(dir)];
403            ret.extend(attrs.into_iter().map(ElemProp::Attr));
404            return ret;
405        }
406        attrs
407            .into_iter()
408            .map(|attr| {
409                if dir_parser.detect_directive(&attr) {
410                    // TODO: report duplicate prop by is_mergeable_prop
411                    ElemProp::Dir(dir_parser.parse(attr))
412                } else {
413                    ElemProp::Attr(attr)
414                }
415            })
416            .collect()
417    }
418
419    fn handle_pre_like(&mut self, elem: &Element) {
420        debug_assert!(
421            self.open_elems
422                .last()
423                .map_or(true, |e| e.location != elem.location),
424            "element should not be pushed to stack yet.",
425        );
426        // increment_pre
427        if (self.option.is_pre_tag)(elem.tag_name) {
428            self.pre_count += 1;
429        }
430        // open_v_pre
431        if is_v_pre_boundary(elem) {
432            debug_assert!(self.v_pre_index.is_none());
433            self.v_pre_index = Some(self.open_elems.len());
434        }
435    }
436    fn parse_end_tag(&mut self, end_tag: &'a str) {
437        // rfind is good since only mismatch will traverse stack
438        let index = self
439            .open_elems
440            .iter()
441            .enumerate()
442            .rfind(|p| element_matches_end_tag(p.1, end_tag))
443            .map(|p| p.0);
444        if let Some(i) = index {
445            let mut to_close = self.open_elems.len() - i;
446            while to_close > 0 {
447                to_close -= 1;
448                self.close_element(to_close == 0);
449            }
450            debug_assert_eq!(self.open_elems.len(), i);
451        } else {
452            let start = self.tokens.last_position();
453            let loc = self.tokens.get_location_from(start);
454            self.emit_error(ErrorKind::InvalidEndTag, loc);
455        }
456    }
457    fn close_element(&mut self, has_matched_end: bool) {
458        let mut elem = self.open_elems.pop().unwrap();
459        self.set_scanner_flag();
460        let start = elem.location.start;
461        if !has_matched_end {
462            // should only span the start of a tag, not the whole tag.
463            let err_location = SourceLocation {
464                start: start.clone(),
465                end: start.clone(),
466            };
467            self.emit_error(ErrorKind::MissingEndTag, err_location);
468        }
469        let location = self.tokens.get_location_from(start);
470        elem.location = location;
471        if self.pre_count > 0 {
472            self.decrement_pre(&mut elem)
473        } else if (self.option.get_text_mode)(elem.tag_name) == TextMode::Data {
474            // skip compress in pre or RAWTEXT/RCDATA
475            compress_whitespaces(&mut elem.children, self.need_condense());
476        }
477        let node = self.parse_element(elem);
478        self.insert_node(node);
479    }
480    fn decrement_pre(&mut self, elem: &mut Element) {
481        debug_assert!(self.pre_count > 0);
482        let pre_boundary = (self.option.is_pre_tag)(elem.tag_name);
483        // trim pre tag's leading new line
484        // https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
485        if !pre_boundary {
486            return;
487        }
488        if let Some(AstNode::Text(tn)) = elem.children.last_mut() {
489            tn.trim_leading_newline();
490        }
491        self.pre_count -= 1;
492    }
493    fn close_v_pre(&mut self) {
494        let idx = self.v_pre_index.unwrap();
495        debug_assert!(idx <= self.open_elems.len());
496        // met v-pre boundary, switch back
497        if idx == self.open_elems.len() {
498            self.v_pre_index = None;
499        }
500    }
501    fn parse_element(&mut self, mut elem: Element<'a>) -> AstNode<'a> {
502        debug_assert!(elem.tag_type == ElementType::Plain);
503        if self.v_pre_index.is_some() {
504            debug_assert!({
505                let i = *self.v_pre_index.as_ref().unwrap();
506                i != self.open_elems.len() || is_v_pre_boundary(&elem)
507            });
508            self.close_v_pre();
509            elem.tag_type = ElementType::Plain;
510        } else if elem.tag_name == "slot" {
511            elem.tag_type = ElementType::SlotOutlet;
512        } else if is_template_element(&elem) {
513            elem.tag_type = ElementType::Template;
514        } else if self.is_component(&elem) {
515            elem.tag_type = ElementType::Component;
516        }
517        AstNode::Element(elem)
518    }
519    fn parse_text(&mut self, text: VStr<'a>) {
520        let mut text = smallvec![text];
521        let mut next_token = None;
522        let start = self.tokens.last_position();
523        for token in &mut self.tokens {
524            if let Token::Text(ds) = token {
525                text.push(ds);
526            } else {
527                next_token = Some(token);
528                break;
529            }
530        }
531        let end = self.tokens.last_position();
532        let location = SourceLocation { start, end };
533        let text_node = TextNode { text, location };
534        self.insert_node(AstNode::Text(text_node));
535        // NB: token must not be dropped
536        if let Some(token) = next_token {
537            self.parse_token(token);
538        }
539    }
540    fn parse_comment(&mut self, c: &'a str) {
541        // Remove comments if desired by configuration.
542        if !self.option.preserve_comment {
543            return;
544        }
545        let pos = self.tokens.last_position();
546        let source_node = SourceNode {
547            source: c,
548            location: self.tokens.get_location_from(pos),
549        };
550        self.insert_node(AstNode::Comment(source_node));
551    }
552    fn parse_interpolation(&mut self, src: &'a str) {
553        let pos = self.tokens.last_position();
554        let source_node = SourceNode {
555            source: src,
556            location: self.tokens.get_location_from(pos),
557        };
558        self.insert_node(AstNode::Interpolation(source_node));
559    }
560
561    // https://html.spec.whatwg.org/multipage/parsing.html#parse-error-eof-in-script-html-comment-like-text
562    fn report_unclosed_script_comment(&mut self) {
563        debug_assert!(self.tokens.next().is_none());
564        let elem = match self.open_elems.last() {
565            Some(e) => e,
566            None => return,
567        };
568        if !elem.tag_name.eq_ignore_ascii_case("script") {
569            return;
570        }
571        let text = match elem.children.first() {
572            Some(AstNode::Text(text)) => text,
573            _ => return,
574        };
575        // Netscape's legacy from 1995 when JS is nascent.
576        // Even 4 years before Bizarre Summer(?v=UztXN2rKQNc).
577        // https://stackoverflow.com/questions/808816/
578        if text.contains("<!--") && !text.contains("-->") {
579            let loc = SourceLocation {
580                start: self.tokens.last_position(),
581                end: self.tokens.last_position(),
582            };
583            self.emit_error(ErrorKind::EofInScriptHtmlCommentLikeText, loc);
584        }
585    }
586
587    // must call this when handle CDATA
588    #[inline]
589    fn set_scanner_flag(&mut self) {
590        if self.need_flag_namespace {
591            return;
592        }
593        // TODO: we can set flag only when namespace changes
594        let in_html = self
595            .open_elems
596            .last()
597            .map_or(true, |e| e.namespace == Namespace::Html);
598        self.tokens.set_is_in_html(in_html)
599    }
600
601    fn is_component(&self, e: &Element) -> bool {
602        let opt = &self.option;
603        let tag_name = e.tag_name;
604        if (opt.is_custom_element)(tag_name) {
605            return false;
606        }
607        if tag_name == "component"
608            || tag_name.starts_with(|c| matches!(c, 'A'..='Z'))
609            || is_core_component(tag_name)
610            || (opt.is_builtin_component)(tag_name)
611            || !(opt.is_native_element)(tag_name)
612        {
613            return true;
614        }
615        e.properties.iter().any(|prop| match prop {
616            ElemProp::Dir(Directive { name: "is", .. }) => true,
617            ElemProp::Attr(Attribute {
618                name: "is",
619                value: Some(v),
620                ..
621            }) => v.content.starts_with("vue:"),
622            _ => false,
623        })
624    }
625
626    fn need_condense(&self) -> bool {
627        matches!(self.option.whitespace, WhitespaceStrategy::Condense)
628    }
629}
630
631const BIND_CHAR: char = ':';
632const MOD_CHAR: char = '.';
633const ON_CHAR: char = '@';
634const SLOT_CHAR: char = '#';
635const SEP_BYTES: &[u8] = &[BIND_CHAR as u8, MOD_CHAR as u8];
636const SHORTHANDS: &[char] = &[BIND_CHAR, ON_CHAR, SLOT_CHAR, MOD_CHAR];
637const DIR_MARK: &str = "v-";
638
639type StrPair<'a> = (&'a str, &'a str);
640struct DirectiveParser<'a, 'e, Eh: ErrorHandler> {
641    eh: &'e Eh,
642    name_loc: SourceLocation,
643    location: SourceLocation,
644    cached: Option<StrPair<'a>>,
645}
646impl<'a, 'e, Eh: ErrorHandler> DirectiveParser<'a, 'e, Eh> {
647    fn new(eh: &'e Eh) -> Self {
648        Self {
649            eh,
650            name_loc: Default::default(),
651            location: Default::default(),
652            cached: None,
653        }
654    }
655    fn attr_name_err(&self, kind: ErrorKind) {
656        let error = CompilationError::new(kind).with_location(self.name_loc.clone());
657        self.eh.on_error(error);
658    }
659    fn detect_directive(&mut self, attr: &Attribute<'a>) -> bool {
660        debug_assert!(self.cached.is_none());
661        self.cached = self.detect_dir_name(attr);
662        self.cached.is_some()
663    }
664    fn set_location(&mut self, attr: &Attribute<'a>) {
665        self.location = attr.location.clone();
666        self.name_loc = attr.name_loc.clone();
667    }
668
669    fn parse(&mut self, attr: Attribute<'a>) -> Directive<'a> {
670        let (name, prefixed) = self
671            .cached
672            .or_else(|| self.detect_dir_name(&attr))
673            .expect("Parse without detection requires attribute be directive.");
674        let is_prop = attr.name.starts_with('.');
675        let is_v_slot = name == "slot";
676        let (arg_str, mods_str) = self.split_arg_and_mods(prefixed, is_v_slot, is_prop);
677        let argument = self.parse_directive_arg(arg_str);
678        let modifiers = self.parse_directive_mods(mods_str, is_prop);
679        self.cached = None; // cleanup
680        Directive {
681            name,
682            argument,
683            modifiers,
684            expression: attr.value,
685            head_loc: attr.name_loc,
686            location: attr.location,
687        }
688    }
689    // NB: this function sets self's location so it's mut.
690    fn detect_dir_name(&mut self, attr: &Attribute<'a>) -> Option<StrPair<'a>> {
691        self.set_location(attr);
692        self.parse_dir_name(attr)
693    }
694    // Returns the directive name and shorthand-prefixed arg/mod str, if any.
695    fn parse_dir_name(&self, attr: &Attribute<'a>) -> Option<StrPair<'a>> {
696        let name = attr.name;
697        if !name.starts_with(DIR_MARK) {
698            let ret = match name.chars().next()? {
699                BIND_CHAR | MOD_CHAR => "bind",
700                ON_CHAR => "on",
701                SLOT_CHAR => "slot",
702                _ => return None,
703            };
704            return Some((ret, name));
705        }
706        let n = &name[2..];
707        let ret = n
708            .bytes()
709            .position(|c| SEP_BYTES.contains(&c))
710            .map(|i| n.split_at(i))
711            .unwrap_or((n, ""));
712        if ret.0.is_empty() {
713            self.attr_name_err(ErrorKind::MissingDirectiveName);
714            return None;
715        }
716        Some(ret)
717    }
718    // Returns arg without shorthand/separator and dot-leading mods
719    fn split_arg_and_mods(&self, prefixed: &'a str, is_v_slot: bool, is_prop: bool) -> StrPair<'a> {
720        // prefixed should either be empty or starts with shorthand.
721        debug_assert!(prefixed.is_empty() || prefixed.starts_with(SHORTHANDS));
722        if prefixed.is_empty() {
723            return ("", "");
724        }
725        if prefixed.len() == 1 {
726            self.attr_name_err(ErrorKind::MissingDirectiveArg);
727            return ("", "");
728        }
729        let remain = &prefixed[1..];
730        // bind/on/customDir accept arg, mod. slot accepts nothing.
731        // see vuejs/vue-next#1241 special case for v-slot
732        if is_v_slot {
733            if prefixed.starts_with(MOD_CHAR) {
734                // only . can end dir_name, e.g. v-slot.error
735                self.attr_name_err(ErrorKind::InvalidVSlotModifier);
736                ("", prefixed)
737            } else {
738                debug_assert!(prefixed.starts_with(&[SLOT_CHAR, BIND_CHAR][..]));
739                (remain, "")
740            }
741        } else if prefixed.starts_with(MOD_CHAR) && !is_prop {
742            // handle v-dir.arg, only .prop expect argument
743            ("", prefixed)
744        } else if remain.starts_with('[') {
745            self.split_dynamic_arg(remain)
746        } else {
747            debug_assert!(!prefixed.starts_with(SLOT_CHAR));
748            // handle .prop shorthand elsewhere
749            remain
750                .bytes()
751                .position(|u| u == MOD_CHAR as u8)
752                .map(|i| remain.split_at(i))
753                .unwrap_or((remain, ""))
754        }
755    }
756    fn split_dynamic_arg(&self, remain: &'a str) -> (&'a str, &'a str) {
757        // dynamic arg
758        let bytes = remain.as_bytes();
759        let end = bytes
760            .iter()
761            .position(|b| *b == b']')
762            .map_or(bytes.len(), |i| i + 1);
763        let (arg, mut mods) = remain.split_at(end);
764        if mods.starts_with(|c| c != MOD_CHAR) {
765            self.attr_name_err(ErrorKind::UnexpectedContentAfterDynamicDirective);
766            mods = mods.trim_start_matches(|c| c != MOD_CHAR);
767        }
768        (arg, mods)
769    }
770    fn parse_directive_arg(&self, arg: &'a str) -> Option<DirectiveArg<'a>> {
771        if arg.is_empty() {
772            return None;
773        }
774        Some(if !arg.starts_with('[') {
775            DirectiveArg::Static(arg)
776        } else if let Some(i) = arg.chars().position(|c| c == ']') {
777            debug_assert!(i == arg.len() - 1);
778            DirectiveArg::Dynamic(&arg[1..i])
779        } else {
780            self.attr_name_err(ErrorKind::MissingDynamicDirectiveArgumentEnd);
781            DirectiveArg::Dynamic(&arg[1..])
782        })
783    }
784    fn parse_directive_mods(&self, mods: &'a str, is_prop: bool) -> Vec<&'a str> {
785        debug_assert!(mods.is_empty() || mods.starts_with(MOD_CHAR));
786        let report_missing_mod = |s: &&str| {
787            if s.is_empty() {
788                self.attr_name_err(ErrorKind::MissingDirectiveMod);
789            }
790        };
791        let mut ret = if mods.is_empty() {
792            vec![]
793        } else {
794            mods[1..]
795                .as_bytes()
796                .split(|b| *b == b'.')
797                .map(std::str::from_utf8) // use unsafe if too slow
798                .map(Result::unwrap)
799                .inspect(report_missing_mod)
800                .collect()
801        };
802        if is_prop {
803            ret.push("prop")
804        }
805        ret
806    }
807}
808
809fn compress_whitespaces(nodes: &mut Vec<AstNode>, need_condense: bool) {
810    // no two consecutive Text node, ensured by parse_text
811    debug_assert!({
812        let no_consecutive_text = |last_is_text, is_text| {
813            if last_is_text && is_text {
814                None
815            } else {
816                Some(is_text)
817            }
818        };
819        nodes
820            .iter()
821            .map(|n| matches!(n, AstNode::Text(_)))
822            .try_fold(false, no_consecutive_text)
823            .is_some()
824    });
825    let mut i = 0;
826    while i < nodes.len() {
827        let should_remove = if let AstNode::Text(child) = &nodes[i] {
828            use AstNode as A;
829            if !child.is_all_whitespace() {
830                // non empty text node
831                if need_condense {
832                    compress_text_node(&mut nodes[i]);
833                }
834                false
835            } else if i == nodes.len() - 1 || i == 0 {
836                // Remove the leading/trailing whitespace
837                true
838            } else if !need_condense {
839                false
840            } else {
841                // Condense mode remove whitespaces between comment and
842                // whitespaces with contains newline between two elements
843                let prev = &nodes[i - 1];
844                let next = &nodes[i + 1];
845                match (prev, next) {
846                    (A::Comment(_), A::Comment(_)) => true,
847                    _ => is_element(prev) && is_element(next) && child.contains(&['\r', '\n'][..]),
848                }
849            }
850        } else {
851            false
852        };
853        if should_remove {
854            nodes.remove(i);
855        } else {
856            i += 1;
857        }
858    }
859}
860
861#[inline]
862fn is_element(n: &AstNode) -> bool {
863    n.get_element().is_some()
864}
865
866fn compress_text_node(n: &mut AstNode) {
867    if let AstNode::Text(src) = n {
868        for s in src.text.iter_mut() {
869            s.compress_whitespace();
870        }
871    } else {
872        debug_assert!(false, "impossible");
873    }
874}
875
876fn is_special_template_directive(n: &str) -> bool {
877    // we only have 5 elements to compare. == takes 2ns while phf takes 26ns
878    match n.len() {
879        2 => n == "if",
880        3 => n == "for",
881        4 => n == "else" || n == "slot",
882        7 => n == "else-if",
883        _ => false,
884    }
885}
886
887fn is_template_element(e: &Element) -> bool {
888    e.tag_name == "template" && find_dir(e, is_special_template_directive).is_some()
889}
890
891fn element_matches_end_tag(e: &Element, tag: &str) -> bool {
892    e.tag_name.eq_ignore_ascii_case(tag)
893}
894
895fn is_v_pre_boundary(elem: &Element) -> bool {
896    find_dir(elem, "pre").is_some()
897}
898
899#[cfg(test)]
900pub mod test {
901    use super::*;
902    use crate::{cast, error::test::TestErrorHandler, scanner::test::base_scan};
903
904    #[test]
905    fn test_parse_text() {
906        let case = "hello {{world}}<p/><p/>";
907        let ast = base_parse(case);
908        let mut children = ast.children;
909        assert_eq!(children.len(), 4);
910        children.pop();
911        children.pop();
912        let world = children.pop().unwrap();
913        let hello = children.pop().unwrap();
914        let v = cast!(hello, AstNode::Text);
915        assert_eq!(v.text[0].raw, "hello ");
916        let v = cast!(world, AstNode::Interpolation);
917        assert_eq!(v.source, "world");
918    }
919
920    pub fn base_parse(s: &str) -> AstRoot {
921        let tokens = base_scan(s);
922        let parser = Parser::new(ParseOption {
923            is_native_element: |s| s != "comp",
924            ..Default::default()
925        });
926        let eh = TestErrorHandler;
927        parser.parse(tokens, eh)
928    }
929
930    pub fn mock_element(s: &str) -> Element {
931        let mut m = base_parse(s).children;
932        m.pop().unwrap().into_element()
933    }
934}