vue_sfc/parser/
mod.rs

1use std::borrow::Cow;
2
3pub use self::error::ParseError;
4use self::util::{parse_start_tag, trim_start_newlines_end};
5
6use crate::{
7    parser::util::parse_end_tag, Attribute, AttributeValue, Block, BlockName, Raw, Section,
8};
9
10mod error;
11mod util;
12
13/// Represent the state of the parser.
14#[derive(Debug)]
15enum State<'a> {
16    /// When the parser is at root level.
17    Root,
18    /// When the parser is in a block in `data state`.
19    /// See <https://html.spec.whatwg.org/multipage/parsing.html#data-state>.
20    Data {
21        name: BlockName<'a>,
22        attributes: Vec<Attribute<'a>>,
23        depth: u16,
24    },
25    /// When the parser is in a block in `RAWTEXT state`.
26    /// See <https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state>.
27    RawText {
28        name: BlockName<'a>,
29        attributes: Vec<Attribute<'a>>,
30    },
31}
32
33/// Parse the given input as a Vue SFC.
34///
35/// # Errors
36/// Will return an error if parsing fails.
37///
38/// # Example
39/// ```rust
40/// use vue_sfc::{Section, Block};
41///
42/// let sfc = vue_sfc::parse("<!-- your input -->").unwrap();
43///
44/// for section in sfc {
45///     match section {
46///         Section::Block(Block { name, attributes, content }) => {
47///             println!(
48///                 "Got a block named `{}` with {} attributes, content is {} bytes long.",
49///                 name,
50///                 attributes.len(),
51///                 content.len()
52///             )
53///         }
54///         Section::Raw(content) => {
55///             println!(
56///                 "Got a raw section, {} bytes long.",
57///                 content.len()
58///             )
59///         }
60///     }
61/// }
62/// ```
63pub fn parse(input: &str) -> Result<Vec<Section<'_>>, ParseError> {
64    let mut less_than_symbols = memchr::memmem::find_iter(input.as_bytes(), "<");
65
66    let mut buffer = Vec::new();
67    let mut offset = 0;
68    let mut state = State::Root;
69
70    loop {
71        match state {
72            State::Root => {
73                let index = if let Some(index) = less_than_symbols.next() {
74                    index
75                } else {
76                    let raw = trim_start_newlines_end(&input[offset..]);
77
78                    if !raw.is_empty() {
79                        // SAFETY: `raw` is end-trimmed and non-empty.
80                        let raw = unsafe { Raw::from_cow_unchecked(Cow::Borrowed(raw)) };
81                        buffer.push(Section::Raw(raw));
82                    }
83
84                    break;
85                };
86
87                if let Ok((_, name)) = parse_end_tag(&input[index..]) {
88                    return Err(ParseError::UnexpectedEndTag(name.as_str().to_owned()));
89                }
90
91                if let Ok((remaining, (name, attributes))) = parse_start_tag(&input[index..]) {
92                    let raw = trim_start_newlines_end(&input[offset..index]);
93
94                    if !raw.is_empty() {
95                        // SAFETY: `raw` is end-trimmed and non-empty.
96                        let raw = unsafe { Raw::from_cow_unchecked(Cow::Borrowed(raw)) };
97                        buffer.push(Section::Raw(raw));
98                    }
99
100                    let raw_text = name.as_str() != "template"
101                        || attributes.iter().any(|(name, value)| {
102                            matches!(
103                                (name.as_str(), value.as_ref().map(AttributeValue::as_str)),
104                                ("lang", Some(lang)) if lang != "html"
105                            )
106                        });
107
108                    offset = input.len() - remaining.len();
109                    state = if raw_text {
110                        State::RawText { name, attributes }
111                    } else {
112                        State::Data {
113                            name,
114                            attributes,
115                            depth: 0,
116                        }
117                    };
118                }
119            }
120            State::Data {
121                name: ref parent_name,
122                ref mut attributes,
123                ref mut depth,
124            } => {
125                let index = less_than_symbols
126                    .next()
127                    .ok_or_else(|| ParseError::MissingEndTag(parent_name.as_str().to_owned()))?;
128
129                match parse_end_tag(&input[index..]) {
130                    Ok((remaining, name)) if &name == parent_name => {
131                        if *depth == 0 {
132                            buffer.push(Section::Block(Block {
133                                name,
134                                attributes: std::mem::take(attributes),
135                                content: Cow::Borrowed(trim_start_newlines_end(
136                                    &input[offset..index],
137                                )),
138                            }));
139
140                            offset = input.len() - remaining.len();
141                            state = State::Root;
142                        } else {
143                            *depth -= 1;
144                        }
145
146                        // Skip start tag check.
147                        continue;
148                    }
149                    _ => { /* Ignore parsing failure & non-matching end tag. */ }
150                }
151
152                match parse_start_tag(&input[index..]) {
153                    Ok((_, (name, _))) if &name == parent_name => {
154                        *depth += 1;
155                    }
156                    _ => { /* Ignore parsing failure & non-matching start tag. */ }
157                }
158            }
159            State::RawText {
160                name: ref parent_name,
161                ref mut attributes,
162            } => {
163                let index = less_than_symbols
164                    .next()
165                    .ok_or_else(|| ParseError::MissingEndTag(parent_name.as_str().to_owned()))?;
166
167                match parse_end_tag(&input[index..]) {
168                    Ok((remaining, name)) if &name == parent_name => {
169                        buffer.push(Section::Block(Block {
170                            name,
171                            attributes: std::mem::take(attributes),
172                            content: Cow::Borrowed(trim_start_newlines_end(&input[offset..index])),
173                        }));
174
175                        offset = input.len() - remaining.len();
176                        state = State::Root;
177                    }
178                    _ => { /* Ignore non-matching end tags. */ }
179                }
180            }
181        }
182    }
183
184    Ok(buffer)
185}
186
187#[cfg(test)]
188mod tests {
189    use std::borrow::Cow;
190
191    use crate::{Block, BlockName, Raw, Section};
192
193    use super::parse;
194
195    #[test]
196    fn test_parse_empty() {
197        assert_eq!(parse("").unwrap(), vec![]);
198    }
199
200    #[test]
201    fn test_parse_raw() {
202        assert_eq!(
203            parse("<!-- a comment -->").unwrap(),
204            vec![Section::Raw(Raw::try_from("<!-- a comment -->").unwrap())]
205        );
206    }
207
208    #[test]
209    fn test_parse_block() {
210        assert_eq!(
211            parse("<template></template>").unwrap(),
212            vec![Section::Block(Block {
213                name: BlockName::try_from("template").unwrap(),
214                attributes: vec![],
215                content: Cow::default()
216            })]
217        );
218    }
219
220    #[test]
221    fn test_parse_consecutive_blocks() {
222        assert_eq!(
223            parse("<template></template><script></script>").unwrap(),
224            vec![
225                Section::Block(Block {
226                    name: BlockName::try_from("template").unwrap(),
227                    attributes: vec![],
228                    content: Cow::default()
229                }),
230                Section::Block(Block {
231                    name: BlockName::try_from("script").unwrap(),
232                    attributes: vec![],
233                    content: Cow::default()
234                })
235            ]
236        );
237    }
238
239    #[test]
240    fn test_parse() {
241        let raw = r#"<template>
242  <router-view v-slot="{ Component }"
243  >
244    <suspense v-if="Component" :timeout="150">
245      <template #default>
246        <component :is="Component"/>
247      </template>
248      <template #fallback>
249        Loading...
250      </template>
251    </suspense>
252  </router-view>
253</template>
254
255<script lang="ts" setup>
256onErrorCaptured((err) => {
257  console.error(err);
258});
259</script>"#;
260
261        let sfc = parse(raw).unwrap();
262
263        match &sfc[0] {
264            Section::Block(Block {
265                name,
266                attributes,
267                content,
268            }) => {
269                assert_eq!(name.as_str(), "template");
270                assert_eq!(content.len(), 266);
271                assert!(attributes.is_empty());
272            }
273            _ => panic!("expected a block"),
274        }
275
276        match &sfc[1] {
277            Section::Block(Block {
278                name,
279                attributes,
280                content,
281            }) => {
282                assert_eq!(name.as_str(), "script");
283                assert_eq!(content.len(), 52);
284                assert_eq!(attributes[0].0.as_str(), "lang");
285                assert_eq!(attributes[0].1.as_ref().unwrap().as_str(), "ts");
286                assert_eq!(attributes[1].0.as_str(), "setup");
287                assert!(attributes[1].1.is_none());
288            }
289            _ => panic!("expected a block"),
290        }
291    }
292}