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 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 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
195pub 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 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 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}