fervid_parser/
attributes.rs

1use fervid_core::{
2    AttributeOrBinding, FervidAtom, StrOrExpr, VBindDirective, VCustomDirective, VForDirective,
3    VModelDirective, VOnDirective, VSlotDirective, VueDirectives,
4};
5use swc_core::{common::{BytePos, Span}, ecma::ast::Expr};
6use swc_ecma_parser::Syntax;
7use swc_html_ast::Attribute;
8
9use crate::{
10    error::{ParseError, ParseErrorKind},
11    SfcParser,
12};
13
14impl SfcParser<'_, '_, '_> {
15    /// Returns `true` when `v-pre` is discovered
16    pub fn process_element_attributes(
17        &mut self,
18        raw_attributes: Vec<Attribute>,
19        attrs_or_bindings: &mut Vec<AttributeOrBinding>,
20        vue_directives: &mut Option<Box<VueDirectives>>,
21    ) -> bool {
22        // Skip any kind of processing for `v-pre` mode
23        if self.is_pre {
24            attrs_or_bindings.extend(raw_attributes.into_iter().map(create_regular_attribute));
25            return false;
26        }
27
28        // Check existence of `v-pre` in attributes
29        let has_v_pre = raw_attributes.iter().any(|attr| attr.name == "v-pre");
30        if has_v_pre {
31            attrs_or_bindings.extend(
32                raw_attributes
33                    .into_iter()
34                    .filter(|attr| attr.name != "v-pre")
35                    .map(create_regular_attribute),
36            );
37            return true;
38        }
39
40        for mut raw_attribute in raw_attributes.into_iter() {
41            // Use raw names for attributes, otherwise SWC transforms them to lowercase
42            // `-1` is needed because SWC spans start from 1
43            let raw_idx_start = raw_attribute.span.lo.0 as usize - 1;
44            let raw_idx_end = raw_idx_start + raw_attribute.name.len();
45            raw_attribute.name = FervidAtom::from(&self.input[raw_idx_start..raw_idx_end]);
46
47            match self.try_parse_directive(raw_attribute, attrs_or_bindings, vue_directives) {
48                Ok(()) => {
49                    // do nothing, we are good already
50                }
51
52                // parse as a raw attribute
53                Err(raw_attribute) => {
54                    attrs_or_bindings.push(create_regular_attribute(raw_attribute))
55                }
56            }
57        }
58
59        // No `v-pre` found in attributes
60        false
61    }
62
63    /// Returns `true` if it was recognized as a directive (regardless if it was successfully parsed)
64    pub fn try_parse_directive(
65        &mut self,
66        raw_attribute: Attribute,
67        attrs_or_bindings: &mut Vec<AttributeOrBinding>,
68        vue_directives: &mut Option<Box<VueDirectives>>,
69    ) -> Result<(), Attribute> {
70        macro_rules! bail {
71            // Everything is okay, this is just not a directive
72            () => {
73                return Err(raw_attribute);
74            };
75            // Parsing directive failed
76            ($err_kind: expr) => {
77                self.errors.push(ParseError {
78                    kind: $err_kind,
79                    span: raw_attribute.span,
80                });
81                return Err(raw_attribute);
82            };
83            (js, $parse_error: expr) => {
84                self.errors.push($parse_error);
85                return Err(raw_attribute);
86            };
87        }
88
89        macro_rules! ts {
90            () => {
91                Syntax::Typescript(Default::default())
92            };
93        }
94
95        // TODO Fix and test parsing of directives
96
97        // TODO Should the span be narrower? (It can be narrowed with lo = lo + name.len() + 1 and hi = hi - 1)
98        let span = raw_attribute.span;
99        let raw_name: &str = &raw_attribute.name;
100        let mut chars_iter = raw_name.chars().enumerate().peekable();
101
102        // Every directive starts with a prefix: `@`, `:`, `.`, `#` or `v-`
103        let Some((_, prefix)) = chars_iter.next() else {
104            bail!(ParseErrorKind::DirectiveSyntax);
105        };
106
107        // https://vuejs.org/api/built-in-directives.html#v-bind
108        let mut is_bind_prop = false;
109        let mut expect_argument = true;
110        let mut argument_start = 0;
111        let mut argument_end = raw_name.len();
112
113        let directive_name = match prefix {
114            '@' => "on",
115            ':' => "bind",
116            '.' => {
117                is_bind_prop = true;
118                "bind"
119            }
120            '#' => "slot",
121            'v' if matches!(chars_iter.next(), Some((_, '-'))) => {
122                // Read directive name
123                let mut start = 0;
124                let mut end = raw_name.len();
125                while let Some((idx, c)) = chars_iter.next() {
126                    if c == '.' {
127                        expect_argument = false;
128                        argument_end = idx;
129                        end = idx;
130                        break;
131                    }
132                    if c == ':' {
133                        end = idx;
134                        break;
135                    }
136                    if start == 0 {
137                        // `idx` is never 0 because zero-th char is `prefix`
138                        start = idx;
139                    }
140                }
141
142                // Directive syntax is bad if we could not read the directive name
143                if start == 0 {
144                    bail!(ParseErrorKind::DirectiveSyntax);
145                }
146
147                &raw_name[start..end]
148            }
149            _ => {
150                bail!();
151            }
152        };
153
154        // Try parsing argument (it is optional and may be empty though)
155        let mut argument: Option<StrOrExpr> = None;
156        if expect_argument {
157            while let Some((idx, c)) = chars_iter.next() {
158                if c == '.' {
159                    argument_end = idx;
160                    break;
161                }
162                if argument_start == 0 {
163                    argument_start = idx;
164                }
165            }
166
167            if argument_start != 0 {
168                let mut raw_argument = &raw_name[argument_start..argument_end];
169                let mut is_dynamic_argument = false;
170
171                // Dynamic argument: `:[dynamic-argument]`
172                if raw_argument.starts_with('[') {
173                    // Check syntax
174                    if !raw_argument.ends_with(']') {
175                        bail!(ParseErrorKind::DynamicArgument);
176                    }
177
178                    raw_argument =
179                        &raw_argument['['.len_utf8()..(raw_argument.len() - ']'.len_utf8())];
180                    if raw_argument.is_empty() {
181                        bail!(ParseErrorKind::DynamicArgument);
182                    }
183
184                    is_dynamic_argument = true;
185                }
186
187                if is_dynamic_argument {
188                    // TODO Narrower span?
189                    let parsed_argument = match self.parse_expr(raw_argument, ts!(), span) {
190                        Ok(parsed) => parsed,
191                        Err(expr_err) => {
192                            bail!(js, expr_err);
193                        }
194                    };
195
196                    argument = Some(StrOrExpr::Expr(parsed_argument));
197                } else {
198                    argument = Some(StrOrExpr::Str(FervidAtom::from(raw_argument)));
199                }
200            }
201        }
202
203        // Try parsing modifiers, it is a simple string split
204        let mut modifiers = Vec::<FervidAtom>::new();
205        if argument_end != 0 {
206            for modifier in raw_name[argument_end..]
207                .split('.')
208                .filter(|m| !m.is_empty())
209            {
210                modifiers.push(FervidAtom::from(modifier));
211            }
212        }
213
214        /// Unwrapping the value or failing
215        macro_rules! expect_value {
216            () => {
217                if let Some(ref value) = raw_attribute.value {
218                    value
219                } else {
220                    bail!(ParseErrorKind::DirectiveSyntax);
221                }
222            };
223        }
224
225        macro_rules! get_directives {
226            () => {
227                vue_directives.get_or_insert_with(|| Box::new(VueDirectives::default()))
228            };
229        }
230
231        macro_rules! push_directive {
232            ($key: ident, $value: expr) => {
233                let directives = get_directives!();
234                directives.$key = Some($value);
235            };
236        }
237
238        macro_rules! push_directive_js {
239            ($key: ident, $value: expr) => {
240                match self.parse_expr($value, ts!(), span) {
241                    Ok(parsed) => {
242                        let directives = get_directives!();
243                        directives.$key = Some(parsed);
244                    }
245                    Result::Err(expr_err) => self.report_error(expr_err),
246                }
247            };
248        }
249
250        // Construct the directives from parts
251        match directive_name {
252            // Directives arranged by estimated usage frequency
253            "bind" => {
254                // Get flags
255                let mut is_camel = false;
256                let mut is_prop = is_bind_prop;
257                let mut is_attr = false;
258                for modifier in modifiers.iter() {
259                    match modifier.as_ref() {
260                        "camel" => is_camel = true,
261                        "prop" => is_prop = true,
262                        "attr" => is_attr = true,
263                        _ => {}
264                    }
265                }
266
267                let value = expect_value!();
268
269                let parsed_expr = match self.parse_expr(&value, ts!(), span) {
270                    Ok(parsed) => parsed,
271                    Err(expr_err) => {
272                        bail!(js, expr_err);
273                    }
274                };
275
276                attrs_or_bindings.push(AttributeOrBinding::VBind(VBindDirective {
277                    argument,
278                    value: parsed_expr,
279                    is_camel,
280                    is_prop,
281                    is_attr,
282                    span,
283                }));
284            }
285
286            "on" => {
287                let handler = match raw_attribute.value {
288                    Some(ref value) => match self.parse_expr(&value, ts!(), span) {
289                        Ok(parsed) => Some(parsed),
290                        Err(expr_err) => {
291                            bail!(js, expr_err);
292                        }
293                    },
294                    None => None,
295                };
296
297                attrs_or_bindings.push(AttributeOrBinding::VOn(VOnDirective {
298                    event: argument,
299                    handler,
300                    modifiers,
301                    span,
302                }));
303            }
304
305            "if" => {
306                let value = expect_value!();
307                push_directive_js!(v_if, &value);
308            }
309
310            "else-if" => {
311                let value = expect_value!();
312                push_directive_js!(v_else_if, &value);
313            }
314
315            "else" => {
316                push_directive!(v_else, ());
317            }
318
319            "for" => {
320                let value = expect_value!();
321
322                let Some(((itervar, itervar_span), (iterable, iterable_span))) =
323                    split_itervar_and_iterable(&value, span)
324                else {
325                    bail!(ParseErrorKind::DirectiveSyntax);
326                };
327
328                match self.parse_expr(itervar, ts!(), itervar_span) {
329                    Ok(itervar) => match self.parse_expr(iterable, ts!(), iterable_span) {
330                        Ok(iterable) => {
331                            push_directive!(
332                                v_for,
333                                VForDirective {
334                                    iterable,
335                                    itervar,
336                                    patch_flags: Default::default(),
337                                    span
338                                }
339                            );
340                        }
341                        Result::Err(expr_err) => self.report_error(expr_err),
342                    },
343                    Result::Err(expr_err) => self.report_error(expr_err),
344                };
345            }
346
347            "model" => {
348                let value = expect_value!();
349
350                match self.parse_expr(&value, ts!(), span) {
351                    Ok(model_binding) => {
352                        // v-model value must be a valid JavaScript member expression
353                        if !matches!(*model_binding, Expr::Member(_) | Expr::Ident(_)) {
354                            // TODO Report an error
355                            bail!();
356                        }
357
358                        let directives = get_directives!();
359                        directives.v_model.push(VModelDirective {
360                            argument,
361                            value: model_binding,
362                            update_handler: None,
363                            modifiers,
364                            span,
365                        });
366                    }
367                    Result::Err(_) => {}
368                }
369            }
370
371            "slot" => {
372                let value =
373                    raw_attribute
374                        .value
375                        .and_then(|v| match self.parse_pat(&v, ts!(), span) {
376                            Ok(value) => Some(Box::new(value)),
377                            Result::Err(_) => None,
378                        });
379                push_directive!(
380                    v_slot,
381                    VSlotDirective {
382                        slot_name: argument,
383                        value,
384                    }
385                );
386            }
387
388            "show" => {
389                let value = expect_value!();
390                push_directive_js!(v_show, &value);
391            }
392
393            "html" => {
394                let value = expect_value!();
395                push_directive_js!(v_html, &value);
396            }
397
398            "text" => {
399                let value = expect_value!();
400                push_directive_js!(v_text, &value);
401            }
402
403            "once" => {
404                push_directive!(v_once, ());
405            }
406
407            "pre" => {
408                push_directive!(v_pre, ());
409            }
410
411            "memo" => {
412                let value = expect_value!();
413                push_directive_js!(v_memo, &value);
414            }
415
416            "cloak" => {
417                push_directive!(v_cloak, ());
418            }
419
420            // Custom
421            _ => 'custom: {
422                // If no value, include as is
423                let Some(value) = raw_attribute.value else {
424                    let directives = get_directives!();
425                    directives.custom.push(VCustomDirective {
426                        name: directive_name.into(),
427                        argument,
428                        modifiers,
429                        value: None,
430                    });
431                    break 'custom;
432                };
433
434                // If there is a value, try parsing it and only include the successfully parsed values
435                match self.parse_expr(&value, ts!(), span) {
436                    Ok(parsed) => {
437                        let directives = get_directives!();
438                        directives.custom.push(VCustomDirective {
439                            name: directive_name.into(),
440                            argument,
441                            modifiers,
442                            value: Some(parsed),
443                        });
444                    }
445                    Result::Err(expr_err) => self.report_error(expr_err),
446                }
447            }
448        }
449
450        Ok(())
451    }
452}
453
454/// Creates `AttributeOrBinding::RegularAttribute`
455#[inline]
456pub fn create_regular_attribute(raw_attribute: Attribute) -> AttributeOrBinding {
457    AttributeOrBinding::RegularAttribute {
458        name: raw_attribute.name,
459        value: raw_attribute.value.unwrap_or_default(),
460        span: raw_attribute.span,
461    }
462}
463
464fn split_itervar_and_iterable<'a>(
465    raw: &'a str,
466    original_span: Span,
467) -> Option<((&'a str, Span), (&'a str, Span))> {
468    // `item in iterable` or `item of iterable`
469    let Some(split_idx) = raw.find(" in ").or_else(|| raw.find(" of ")) else {
470        return None;
471    };
472    const SPLIT_LEN: usize = " in ".len();
473
474    // Get the trimmed itervar and its span
475    let mut offset = original_span.lo.0;
476    let mut itervar = &raw[..split_idx];
477    let mut itervar_old_len = itervar.len();
478    itervar = itervar.trim_start();
479    let itervar_lo = BytePos(offset + (itervar_old_len - itervar.len()) as u32);
480    itervar_old_len = itervar.len();
481    itervar = itervar.trim_end();
482    let itervar_hi = BytePos(offset + (split_idx - (itervar_old_len - itervar.len())) as u32);
483
484    let iterable_start = split_idx + SPLIT_LEN;
485    offset += iterable_start as u32;
486
487    let mut iterable = &raw[iterable_start..];
488    let iterable_old_len = iterable.len();
489    iterable = iterable.trim_start();
490    let iterable_lo = BytePos(offset + (iterable_old_len - iterable.len()) as u32);
491    iterable = iterable.trim_end();
492    let iterable_hi = BytePos(iterable_lo.0 + iterable.len() as u32);
493
494    if itervar.is_empty() || iterable.is_empty() {
495        return None;
496    }
497
498    let new_span_itervar = Span {
499        lo: itervar_lo,
500        hi: itervar_hi,
501        ctxt: original_span.ctxt,
502    };
503
504    let new_span_iterable = Span {
505        lo: iterable_lo,
506        hi: iterable_hi,
507        ctxt: original_span.ctxt,
508    };
509
510    Some(((itervar, new_span_itervar), (iterable, new_span_iterable)))
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn it_correctly_splits_itervar_iterable() {
519        macro_rules! check {
520            ($input: expr, $itervar: expr, $itervar_lo: expr, $itervar_hi: expr, $iterable: expr, $iterable_lo: expr, $iterable_hi: expr) => {
521                let input = $input;
522                let span = Span {
523                    lo: BytePos(1),
524                    hi: BytePos((input.len() + 1) as u32),
525                    ctxt: Default::default(),
526                };
527
528                let Some(((itervar, itervar_span), (iterable, iterable_span))) =
529                    split_itervar_and_iterable(input, span)
530                else {
531                    panic!("Did not manage to split")
532                };
533                assert_eq!($itervar, itervar);
534                assert_eq!($itervar_lo, itervar_span.lo.0);
535                assert_eq!($itervar_hi, itervar_span.hi.0);
536                assert_eq!($iterable, iterable);
537                assert_eq!($iterable_lo, iterable_span.lo.0);
538                assert_eq!($iterable_hi, iterable_span.hi.0);
539            };
540        }
541
542        // Trivial (all `Span`s start from 1)
543        check!("item in list", "item", 1, 5, "list", 9, 13);
544        check!("item of list", "item", 1, 5, "list", 9, 13);
545        check!("i in 3", "i", 1, 2, "3", 6, 7);
546
547        // A bit harder
548        check!("   item   in \n \t  list   ", "item", 4, 8, "list", 19, 23);
549    }
550}