rsass/parser/
strings.rs

1use super::value::value_expression;
2use super::{input_to_str, input_to_string, PResult, Span};
3use crate::sass::{SassString, StringPart};
4use crate::value::Quotes;
5use nom::branch::alt;
6use nom::bytes::complete::{is_a, is_not, tag, take};
7use nom::character::complete::{alphanumeric1, char, one_of};
8use nom::combinator::{
9    cut, map, map_opt, map_res, not, opt, peek, recognize, value, verify,
10};
11use nom::error::context;
12use nom::multi::{fold_many0, fold_many1, many0, many1, many_m_n};
13use nom::sequence::{delimited, pair, preceded, terminated};
14use nom::Parser as _;
15use std::str::from_utf8;
16
17pub fn sass_string(input: Span) -> PResult<SassString> {
18    verify(sass_string_allow_dash, |s| s.single_raw() != Some("-"))
19        .parse(input)
20}
21
22pub fn sass_string_allow_dash(input: Span) -> PResult<SassString> {
23    let (input, first) = alt((
24        string_part_interpolation,
25        map(unquoted_first_part, StringPart::Raw),
26    ))
27    .parse(input)?;
28    let (input, parts) = fold_many0(
29        alt((
30            string_part_interpolation,
31            map(unquoted_part, StringPart::Raw),
32        )),
33        || vec![first.clone()],
34        |mut acc, item| {
35            acc.push(item);
36            acc
37        },
38    )
39    .parse(input)?;
40    Ok((input, SassString::new(parts, Quotes::None)))
41}
42
43pub fn custom_value(input: Span) -> PResult<SassString> {
44    map(
45        context("Expected token.", custom_value_inner),
46        |mut parts| {
47            if let Some(StringPart::Raw(last)) = parts.last_mut() {
48                if last.ends_with('\n') {
49                    last.pop();
50                    last.push(' ');
51                }
52            }
53            SassString::new(parts, Quotes::None)
54        },
55    )
56    .parse(input)
57}
58pub fn custom_value_inner(input: Span) -> PResult<Vec<StringPart>> {
59    fold_many1(
60        alt((
61            |input| custom_value_paren('[', ']', input),
62            |input| custom_value_paren('{', '}', input),
63            |input| custom_value_paren('(', ')', input),
64            map(sass_string_dq, |mut s| {
65                s.prepend("\"");
66                s.append_str("\"");
67                s.parts()
68            }),
69            map(sass_string_sq, |mut s| {
70                s.prepend("'");
71                s.append_str("'");
72                s.parts()
73            }),
74            map(string_part_interpolation, |s| vec![s]),
75            map(unquoted_part, |s| vec![StringPart::Raw(s)]),
76            map_opt(is_not("\"'#\\;{}()[]"), |s: Span| {
77                if s.is_empty() {
78                    None
79                } else {
80                    Some(vec![StringPart::from(input_to_str(s).ok()?)])
81                }
82            }),
83        )),
84        Vec::new,
85        |mut acc, items: Vec<StringPart>| {
86            acc.extend(items);
87            acc
88        },
89    )
90    .parse(input)
91}
92
93fn custom_value_paren(
94    start: char,
95    end: char,
96    input: Span,
97) -> PResult<Vec<StringPart>> {
98    map(
99        delimited(
100            char(start),
101            fold_many0(
102                alt((
103                    map(tag(";"), |_| vec![StringPart::from(";")]),
104                    custom_value_inner,
105                )),
106                || vec![StringPart::Raw(start.into())],
107                |mut acc, items: Vec<StringPart>| {
108                    acc.extend(items);
109                    acc
110                },
111            ),
112            cut(char(end)),
113        ),
114        |mut parts| {
115            parts.push(StringPart::Raw(end.into()));
116            parts
117        },
118    )
119    .parse(input)
120}
121
122pub fn sass_string_ext(input: Span) -> PResult<SassString> {
123    verify(
124        many1(alt((string_part_interpolation, extended_part)))
125            .map(|parts| SassString::new(parts, Quotes::None)),
126        |s| s.single_raw() != Some("/"),
127    )
128    .parse(input)
129}
130
131fn unquoted_first_part(input: Span) -> PResult<String> {
132    let (input, first) = alt((
133        map(str_plain_part, String::from),
134        normalized_first_escaped_char,
135        map(hash_no_interpolation, String::from),
136    ))
137    .parse(input)?;
138    fold_many0(
139        // Note: This could probably be a whole lot more efficient,
140        // but try to get stuff correct before caring too much about that.
141        alt((
142            map(str_plain_part, String::from),
143            normalized_escaped_char,
144            map(hash_no_interpolation, String::from),
145        )),
146        move || first.clone(),
147        |mut acc: String, item: String| {
148            acc.push_str(&item);
149            acc
150        },
151    )
152    .parse(input)
153}
154fn unquoted_part(input: Span) -> PResult<String> {
155    fold_many1(
156        // Note: This could probably be a whole lot more efficient,
157        // but try to get stuff correct before caring too much about that.
158        alt((
159            map(str_plain_part, String::from),
160            normalized_escaped_char,
161            map(hash_no_interpolation, String::from),
162        )),
163        String::new,
164        |mut acc: String, item: String| {
165            acc.push_str(&item);
166            acc
167        },
168    )
169    .parse(input)
170}
171
172fn normalized_first_escaped_char(input: Span) -> PResult<String> {
173    let (rest, c) = escaped_char(input)?;
174    let result = if c.is_alphabetic() || u32::from(c) >= 0xa1 {
175        format!("{c}")
176    } else if !c.is_control() && !c.is_numeric() && c != '\n' && c != '\t' {
177        format!("\\{c}")
178    } else {
179        format!("\\{:x} ", u32::from(c))
180    };
181    Ok((rest, result))
182}
183fn normalized_escaped_char(input: Span) -> PResult<String> {
184    let (rest, c) = escaped_char(input)?;
185    let result = if c.is_alphanumeric() || c == '-' || u32::from(c) >= 0xa1 {
186        format!("{c}")
187    } else if !c.is_control() && c != '\n' && c != '\t' {
188        format!("\\{c}")
189    } else {
190        format!("\\{:x} ", u32::from(c))
191    };
192    Ok((rest, result))
193}
194
195/// Some special css functions are parsed as plain strings.
196///
197/// For the `calc` function, it is parsed as a string if the arguments
198/// contain a string interpolation, otherwise it is refused by this parser,
199/// so that it can end up beeing parsed as a normal function call.
200pub fn special_function_misc(input: Span) -> PResult<SassString> {
201    let (input, (start, mut args, end)) = verify(
202        (
203            recognize(terminated(
204                alt((
205                    map(
206                        (
207                            opt(delimited(tag("-"), alphanumeric1, tag("-"))),
208                            alt((
209                                tag("calc"),
210                                tag("element"),
211                                tag("env"),
212                                tag("expression"),
213                            )),
214                        ),
215                        |_| (),
216                    ),
217                    map(
218                        preceded(
219                            tag("progid:"),
220                            many1(alt((
221                                map(tag("."), |_| ()),
222                                map(selector_string, |_| ()),
223                            ))),
224                        ),
225                        |_| (),
226                    ),
227                )),
228                tag("("),
229            )),
230            special_args,
231            alt((tag(")"), tag(""))),
232        ),
233        |(start, args, _end)| {
234            start.fragment() != b"calc(" || args.is_interpolated()
235        },
236    )
237    .parse(input)?;
238
239    args.prepend(from_utf8(start.fragment()).unwrap());
240    args.append_str(from_utf8(end.fragment()).unwrap());
241    Ok((input, args))
242}
243
244fn special_args(input: Span) -> PResult<SassString> {
245    let (input, parts) = special_arg_parts(input)?;
246    Ok((input, SassString::new(parts, Quotes::None)))
247}
248pub fn special_arg_parts(input: Span) -> PResult<Vec<StringPart>> {
249    let (input, parts) = many0(alt((
250        map(string_part_interpolation, |part| vec![part]),
251        map(hash_no_interpolation, |s| vec![StringPart::from(s)]),
252        map(dq_parts, |mut v| {
253            v.insert(0, StringPart::from("\""));
254            v.push(StringPart::from("\""));
255            v
256        }),
257        map(delimited(tag("("), special_arg_parts, tag(")")), |mut v| {
258            v.insert(0, StringPart::from("("));
259            v.push(StringPart::from(")"));
260            v
261        }),
262        map(tag("\\)"), |_| vec![StringPart::from("\\)")]),
263        value(vec![StringPart::from(" ")], is_a("\n ")),
264        map(map_res(is_not("#()\"\\;\n "), input_to_str), |s| {
265            vec![StringPart::from(s)]
266        }),
267    )))
268    .parse(input)?;
269    Ok((input, parts.into_iter().flatten().collect()))
270}
271
272pub fn special_url(input: Span) -> PResult<SassString> {
273    let (input, _start) = tag("url(").parse(input)?;
274    let (input, _trim) = many0(is_a(" ")).parse(input)?;
275    let (input, mut parts) = many1(alt((
276        string_part_interpolation,
277        map(unquoted_part, StringPart::Raw),
278        map(
279            map_res(is_a("\":.;,!+/="), input_to_string),
280            StringPart::Raw,
281        ),
282    )))
283    .parse(input)?;
284    let (input, _trim) = many0(is_a(" ")).parse(input)?;
285    let (input, _end) = tag(")").parse(input)?;
286    parts.insert(0, "url(".into());
287    parts.push(")".into());
288    Ok((input, SassString::new(parts, Quotes::None)))
289}
290
291pub fn sass_string_dq(input: Span) -> PResult<SassString> {
292    let (input, mut parts) = dq_parts(input)?;
293    cleanup_escape_ws(&mut parts);
294    Ok((input, SassString::new(parts, Quotes::Double)))
295}
296
297fn dq_parts(input: Span) -> PResult<Vec<StringPart>> {
298    delimited(
299        tag("\""),
300        many0(alt((
301            simple_qstring_part,
302            string_part_interpolation,
303            map(hash_no_interpolation, StringPart::from),
304            value(StringPart::Raw("\"".to_string()), tag("\\\"")),
305            value(StringPart::Raw("'".to_string()), tag("'")),
306            map(normalized_escaped_char_q, StringPart::Raw),
307        ))),
308        char('"'),
309    )
310    .parse(input)
311}
312
313pub fn sass_string_sq(input: Span) -> PResult<SassString> {
314    let (input, mut parts) = delimited(
315        tag("'"),
316        many0(alt((
317            simple_qstring_part,
318            string_part_interpolation,
319            map(hash_no_interpolation, StringPart::from),
320            value(StringPart::from("'"), tag("\\'")),
321            value(StringPart::from("\""), tag("\"")),
322            value(StringPart::from(""), tag("\\\n")),
323            map(normalized_escaped_char_q, StringPart::Raw),
324        ))),
325        char('\''),
326    )
327    .parse(input)?;
328    cleanup_escape_ws(&mut parts);
329    Ok((input, SassString::new(parts, Quotes::Single)))
330}
331
332fn cleanup_escape_ws(parts: &mut [StringPart]) {
333    let mut t_iter = parts.iter_mut().peekable();
334    while let Some(ref mut item) = t_iter.next() {
335        if let StringPart::Raw(ref mut s) = item {
336            if s.starts_with('\\') && s.ends_with(' ') {
337                match t_iter.peek() {
338                    None => {
339                        s.pop();
340                    }
341                    Some(StringPart::Raw(next)) => {
342                        if let Some(next) = next.chars().next() {
343                            if !next.is_ascii_hexdigit() && next != '\t' {
344                                s.pop();
345                            }
346                        }
347                    }
348                    _ => (),
349                }
350            }
351        }
352    }
353}
354
355fn normalized_escaped_char_q(input: Span) -> PResult<String> {
356    let (rest, c) = escaped_char(input)?;
357    let result = if c == '\0' {
358        char::REPLACEMENT_CHARACTER.to_string()
359    } else if c.is_control() && c != '\t' {
360        format!("\\{:x} ", u32::from(c))
361    } else if c == '-' || c == '\\' || c == ' ' {
362        format!("\\{c}")
363    } else {
364        c.to_string()
365    };
366    Ok((rest, result))
367}
368
369pub fn string_part_interpolation(input: Span) -> PResult<StringPart> {
370    let (input, expr) =
371        delimited(tag("#{"), value_expression, tag("}")).parse(input)?;
372    Ok((input, StringPart::Interpolation(expr)))
373}
374
375fn simple_qstring_part(input: Span) -> PResult<StringPart> {
376    let (input, part) =
377        map_res(is_not("\\#'\"\n\r\u{c}"), input_to_string).parse(input)?;
378    Ok((input, StringPart::Raw(part)))
379}
380
381fn selector_string(input: Span) -> PResult<String> {
382    fold_many1(
383        // Note: This could probably be a whole lot more efficient,
384        // but try to get stuff correct before caring too much about that.
385        alt((
386            selector_plain_part,
387            map(tag("\\ "), |_| "\\ ".to_string()),
388            map(tag("\\\""), |_| "\\\"".to_string()),
389            map(tag("\\\'"), |_| "\\\'".to_string()),
390            map(tag("\\\\"), |_| "\\\\".to_string()),
391            map(escaped_char, |c| format!("{c}")),
392            map(hash_no_interpolation, String::from),
393        )),
394        String::new,
395        |mut acc: String, item: String| {
396            acc.push_str(&item);
397            acc
398        },
399    )
400    .parse(input)
401}
402
403fn selector_plain_part(input: Span) -> PResult<String> {
404    fold_many1(
405        verify(take_char, |ch| {
406            ch.is_alphanumeric() || *ch == '-' || *ch == '_'
407        }),
408        String::new,
409        |mut acc, chr: char| {
410            acc.push(chr);
411            acc
412        },
413    )
414    .parse(input)
415}
416
417fn str_plain_part(input: Span) -> PResult<&str> {
418    // TODO: This should probably be based on unicode alphanumeric.
419    map_res(is_not("\r\n\t %<>$\"'\\#+*/()[]{}:;,=!&@~"), input_to_str)
420        .parse(input)
421}
422
423fn hash_no_interpolation(input: Span) -> PResult<&str> {
424    map_res(terminated(tag("#"), peek(not(tag("{")))), input_to_str)
425        .parse(input)
426}
427
428pub fn extended_part(input: Span) -> PResult<StringPart> {
429    let (input, part) = map_res(
430        recognize(pair(
431            verify(take_char, is_ext_str_start_char),
432            many0(verify(take_char, is_ext_str_char)),
433        )),
434        input_to_string,
435    )
436    .parse(input)?;
437    Ok((input, StringPart::Raw(part)))
438}
439
440fn is_ext_str_start_char(c: &char) -> bool {
441    is_name_char(c)
442        || *c == '*'
443        || *c == '+'
444        || *c == '.'
445        || *c == '/'
446        || *c == ':'
447        || *c == '='
448        || *c == '?'
449        || *c == '|'
450        || *c == '<'
451        || *c == '>'
452}
453fn is_ext_str_char(c: &char) -> bool {
454    is_name_char(c)
455        || *c == '*'
456        || *c == '+'
457        || *c == ','
458        || *c == '.'
459        || *c == '/'
460        || *c == ':'
461        || *c == '='
462        || *c == '?'
463        || *c == '|'
464        || *c == '<'
465        || *c == '>'
466        || *c == '\\'
467}
468
469pub fn name(input: Span) -> PResult<String> {
470    let (input, first) =
471        verify(alt((escaped_char, take_char)), is_name_start_char)
472            .parse(input)?;
473    verify(
474        fold_many0(
475            alt((escaped_char, name_char)),
476            move || String::from(first),
477            |mut s, c| {
478                s.push(c);
479                s
480            },
481        ),
482        |s: &str| !s.is_empty() && s != "-",
483    )
484    .parse(input)
485}
486
487pub fn unitname(input: Span) -> PResult<String> {
488    let (input, first) =
489        verify(alt((escaped_char, name_char)), |c| c.is_alphabetic())
490            .parse(input)?;
491    fold_many0(
492        verify(alt((escaped_char, name_char)), |c| c.is_alphanumeric()),
493        move || first.to_string(),
494        |mut s, c| {
495            s.push(c);
496            s
497        },
498    )
499    .parse(input)
500}
501
502pub fn name_char(input: Span) -> PResult<char> {
503    verify(take_char, is_name_char).parse(input)
504}
505
506fn escaped_char(input: Span) -> PResult<char> {
507    preceded(
508        tag("\\"),
509        alt((
510            value('\\', tag("\\")),
511            map_opt(
512                pair(
513                    recognize(many_m_n(
514                        1,
515                        6,
516                        one_of("0123456789ABCDEFabcdef"),
517                    )),
518                    alt((
519                        value(true, tag(" ")),
520                        value(
521                            true,
522                            peek(not(one_of("0123456789ABCDEFabcdef"))),
523                        ),
524                        value(false, tag("")),
525                    )),
526                ),
527                |(code, term): (Span, bool)| {
528                    if term || code.len() == 6 {
529                        std::char::from_u32(
530                            u32::from_str_radix(input_to_str(code).ok()?, 16)
531                                .ok()?,
532                        )
533                    } else {
534                        None
535                    }
536                },
537            ),
538            take_char,
539        )),
540    )
541    .parse(input)
542}
543
544fn take_char(input: Span) -> PResult<char> {
545    alt((
546        map_opt(take(1usize), single_char),
547        map_opt(take(2usize), single_char),
548        map_opt(take(3usize), single_char),
549        map_opt(take(4usize), single_char),
550        map_opt(take(5usize), single_char),
551    ))
552    .parse(input)
553}
554
555fn single_char(data: Span) -> Option<char> {
556    from_utf8(data.fragment())
557        .ok()
558        .and_then(|s| s.chars().next())
559}
560
561fn is_name_char(c: &char) -> bool {
562    c.is_alphanumeric() || *c == '_' || *c == '-'
563}
564fn is_name_start_char(c: &char) -> bool {
565    c.is_alphabetic() || *c == '_' || *c == '-'
566}