1use std::borrow::Cow;
2
3use winnow::{
4 combinator::{alt, cut_err, delimited, opt, peek, preceded, repeat, terminated},
5 error::{ContextError, ErrMode, ParseError},
6 prelude::*,
7 stream::{AsChar, ContainsToken, Range},
8 token::{none_of, one_of, take_till, take_until, take_while},
9};
10
11#[derive(Debug, PartialEq)]
17pub struct Global<'s> {
18 pub inner: &'s str,
19 pub outer: &'s str,
20}
21
22#[derive(Debug, PartialEq)]
23pub enum CssFragment<'s> {
24 Class(&'s str),
25 Global(Global<'s>),
26}
27
28pub fn transform_css<'a>(
33 css: &'a str,
34 hash: &str,
35) -> Result<String, ParseError<&'a str, ContextError>> {
36 let fragments = parse_css(css)?;
37
38 let mut new_css = String::with_capacity(css.len() * 2);
39 let mut cursor = css;
40
41 for fragment in fragments {
42 let (span, replace) = match fragment {
43 CssFragment::Class(class) => (class, Cow::Owned(apply_hash(class, hash))),
44 CssFragment::Global(Global { inner, outer }) => (outer, Cow::Borrowed(inner)),
45 };
46
47 let (before, after) = cursor.split_at(span.as_ptr() as usize - cursor.as_ptr() as usize);
48 cursor = &after[span.len()..];
49 new_css.push_str(before);
50 new_css.push_str(&replace);
51 }
52
53 new_css.push_str(cursor);
54 Ok(new_css)
55}
56
57#[allow(clippy::type_complexity)]
60pub fn get_class_mappings<'a>(
61 css: &'a str,
62 hash: &str,
63) -> Result<Vec<(&'a str, Cow<'a, str>)>, ParseError<&'a str, ContextError>> {
64 let fragments = parse_css(css)?;
65 let mut result = Vec::new();
66
67 for c in fragments {
68 match c {
69 CssFragment::Class(class) => {
70 result.push((class, Cow::Owned(apply_hash(class, hash))));
71 }
72 CssFragment::Global(global) => {
73 let global_classes = resolve_global_inner_classes(global)?;
74 result.extend(
75 global_classes
76 .into_iter()
77 .map(|class| (class, Cow::Borrowed(class))),
78 );
79 }
80 }
81 }
82 result.sort_by_key(|e| e.0);
83 result.dedup_by_key(|e| e.0);
84 Ok(result)
85}
86
87fn resolve_global_inner_classes<'a>(
88 global: Global<'a>,
89) -> Result<Vec<&'a str>, ParseError<&'a str, ContextError>> {
90 let input = global.inner;
91 let fragments = selector.parse(input)?;
92 let mut result = Vec::new();
93 for c in fragments {
94 match c {
95 CssFragment::Class(class) => result.push(class),
96 CssFragment::Global(_) => {
97 unreachable!("Top level parser should have already errored if globals are nested")
98 }
99 }
100 }
101 Ok(result)
102}
103
104fn apply_hash(class: &str, hash: &str) -> String {
105 format!("{}-{}", class, hash)
106}
107
108pub fn parse_css(input: &str) -> Result<Vec<CssFragment<'_>>, ParseError<&str, ContextError>> {
111 style_rule_block_contents.parse(input)
112}
113
114fn recognize_repeat<'s, O>(
115 range: impl Into<Range>,
116 f: impl Parser<&'s str, O, ErrMode<ContextError>>,
117) -> impl Parser<&'s str, &'s str, ErrMode<ContextError>> {
118 repeat(range, f).fold(|| (), |_, _| ()).take()
119}
120
121fn ws<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
122 recognize_repeat(
123 0..,
124 alt((
125 line_comment,
126 block_comment,
127 take_while(1.., (AsChar::is_space, '\n', '\r')),
128 )),
129 )
130 .parse_next(input)
131}
132
133fn line_comment<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
134 ("//", take_while(0.., |c| c != '\n'))
135 .take()
136 .parse_next(input)
137}
138
139fn block_comment<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
140 ("/*", cut_err(terminated(take_until(0.., "*/"), "*/")))
141 .take()
142 .parse_next(input)
143}
144
145fn sass_interpolation<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
147 (
148 "#{",
149 cut_err(terminated(take_till(1.., ('{', '}', '\n')), '}')),
150 )
151 .take()
152 .parse_next(input)
153}
154
155fn identifier<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
156 (
157 one_of(('_', '-', AsChar::is_alpha)),
158 take_while(0.., ('_', '-', AsChar::is_alphanum)),
159 )
160 .take()
161 .parse_next(input)
162}
163
164fn class<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
165 preceded('.', identifier).parse_next(input)
166}
167
168fn global<'s>(input: &mut &'s str) -> ModalResult<Global<'s>> {
169 let (inner, outer) = preceded(
170 ":global(",
171 cut_err(terminated(
172 stuff_till(0.., (')', '(', '{')), ')',
174 )),
175 )
176 .with_taken() .parse_next(input)?;
178 Ok(Global { inner, outer })
179}
180
181fn string_dq<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
182 let str_char = alt((none_of(['"']).void(), "\\\"".void()));
183 let str_chars = recognize_repeat(0.., str_char);
184
185 preceded('"', cut_err(terminated(str_chars, '"'))).parse_next(input)
186}
187
188fn string_sq<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
189 let str_char = alt((none_of(['\'']).void(), "\\'".void()));
190 let str_chars = recognize_repeat(0.., str_char);
191
192 preceded('\'', cut_err(terminated(str_chars, '\''))).parse_next(input)
193}
194
195fn string<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
196 alt((string_dq, string_sq)).parse_next(input)
197}
198
199fn stuff_till<'s>(
202 range: impl Into<Range>,
203 list: impl ContainsToken<char>,
204) -> impl Parser<&'s str, &'s str, ErrMode<ContextError>> {
205 recognize_repeat(
206 range,
207 alt((
208 string.void(),
209 block_comment.void(),
210 line_comment.void(),
211 sass_interpolation.void(),
212 '/'.void(),
213 '#'.void(),
214 take_till(1.., ('\'', '"', '/', '#', list)).void(),
215 )),
216 )
217}
218
219fn selector<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
220 repeat(
221 1..,
222 alt((
223 class.map(|c| Some(CssFragment::Class(c))),
224 global.map(|g| Some(CssFragment::Global(g))),
225 ':'.map(|_| None),
226 stuff_till(1.., ('.', ';', '{', '}', ':')).map(|_| None),
227 )),
228 )
229 .fold(Vec::new, |mut acc, item| {
230 if let Some(item) = item {
231 acc.push(item);
232 }
233 acc
234 })
235 .parse_next(input)
236}
237
238fn declaration<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
239 (
240 (opt('$'), identifier),
241 ws,
242 ':',
243 terminated(
244 stuff_till(1.., (';', '{', '}')),
245 alt((';', peek('}'))), ),
247 )
248 .take()
249 .parse_next(input)
250}
251
252fn style_rule_block_statement<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
253 let content = alt((
254 declaration.map(|_| Vec::new()), at_rule,
256 style_rule,
257 ));
258 delimited(ws, content, ws).parse_next(input)
259}
260
261fn style_rule_block_contents<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
262 repeat(0.., style_rule_block_statement)
263 .fold(Vec::new, |mut acc, mut item| {
264 acc.append(&mut item);
265 acc
266 })
267 .parse_next(input)
268}
269
270fn style_rule_block<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
271 preceded(
272 '{',
273 cut_err(terminated(style_rule_block_contents, (ws, '}'))),
274 )
275 .parse_next(input)
276}
277
278fn style_rule<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
279 let (mut classes, mut nested_classes) = (selector, style_rule_block).parse_next(input)?;
280 classes.append(&mut nested_classes);
281 Ok(classes)
282}
283
284fn at_rule<'s>(input: &mut &'s str) -> ModalResult<Vec<CssFragment<'s>>> {
285 let (identifier, char) = preceded(
286 '@',
287 cut_err((
288 terminated(identifier, stuff_till(0.., ('{', '}', ';'))),
289 alt(('{', ';', peek('}'))),
290 )),
291 )
292 .parse_next(input)?;
293
294 if char != '{' {
295 return Ok(vec![]);
296 }
297
298 match identifier {
299 "media" | "layer" | "container" | "include" => {
300 cut_err(terminated(style_rule_block_contents, '}')).parse_next(input)
301 }
302 _ => {
303 cut_err(terminated(unknown_block_contents, '}')).parse_next(input)?;
304 Ok(vec![])
305 }
306 }
307}
308
309fn unknown_block_contents<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
310 recognize_repeat(
311 0..,
312 alt((
313 stuff_till(1.., ('{', '}')).void(),
314 ('{', cut_err((unknown_block_contents, '}'))).void(),
315 )),
316 )
317 .parse_next(input)
318}
319
320#[test]
323fn test_class() {
324 let mut input = "._x1a2b Hello";
325
326 let r = class.parse_next(&mut input);
327 assert_eq!(r, Ok("_x1a2b"));
328}
329
330#[test]
331fn test_selector() {
332 let mut input = ".foo.bar [value=\"fa.sdasd\"] /* .banana */ // .apple \n \t .cry {";
333
334 let r = selector.parse_next(&mut input);
335 assert_eq!(
336 r,
337 Ok(vec![
338 CssFragment::Class("foo"),
339 CssFragment::Class("bar"),
340 CssFragment::Class("cry")
341 ])
342 );
343
344 let mut input = "{";
345
346 let r = selector.take().parse_next(&mut input);
347 assert!(r.is_err());
348}
349
350#[test]
351fn test_declaration() {
352 let mut input = "background-color \t : red;";
353
354 let r = declaration.parse_next(&mut input);
355 assert_eq!(r, Ok("background-color \t : red;"));
356
357 let r = declaration.parse_next(&mut input);
358 assert!(r.is_err());
359}
360
361#[test]
362fn test_style_rule() {
363 let mut input = ".foo.bar {
364 background-color: red;
365 .baz {
366 color: blue;
367 }
368 $some-scss-var: 10px;
369 @some-at-rule blah blah;
370 @media blah .blah {
371 .moo {
372 color: red;
373 }
374 }
375 @container (width > 700px) {
376 .zoo {
377 color: blue;
378 }
379 }
380 }END";
381
382 let r = style_rule.parse_next(&mut input);
383 assert_eq!(
384 r,
385 Ok(vec![
386 CssFragment::Class("foo"),
387 CssFragment::Class("bar"),
388 CssFragment::Class("baz"),
389 CssFragment::Class("moo"),
390 CssFragment::Class("zoo")
391 ])
392 );
393
394 assert_eq!(input, "END");
395}
396
397#[test]
398fn test_at_rule_simple() {
399 let mut input = "@simple-rule blah \"asd;asd\" blah;";
400
401 let r = at_rule.parse_next(&mut input);
402 assert_eq!(r, Ok(vec![]));
403
404 assert!(input.is_empty());
405}
406
407#[test]
408fn test_at_rule_unknown() {
409 let mut input = "@unknown blah \"asdasd\" blah {
410 bunch of stuff {
411 // things inside {
412 blah
413 ' { '
414 }
415
416 .bar {
417 color: blue;
418
419 .baz {
420 color: green;
421 }
422 }
423 }";
424
425 let r = at_rule.parse_next(&mut input);
426 assert_eq!(r, Ok(vec![]));
427
428 assert!(input.is_empty());
429}
430
431#[test]
432fn test_at_rule_media() {
433 let mut input = "@media blah \"asdasd\" blah {
434 .foo {
435 background-color: red;
436 }
437
438 .bar {
439 color: blue;
440
441 .baz {
442 color: green;
443 }
444 }
445 }";
446
447 let r = at_rule.parse_next(&mut input);
448 assert_eq!(
449 r,
450 Ok(vec![
451 CssFragment::Class("foo"),
452 CssFragment::Class("bar"),
453 CssFragment::Class("baz")
454 ])
455 );
456
457 assert!(input.is_empty());
458}
459
460#[test]
461fn test_at_rule_layer() {
462 let mut input = "@layer test {
463 .foo {
464 background-color: red;
465 }
466
467 .bar {
468 color: blue;
469
470 .baz {
471 color: green;
472 }
473 }
474 }";
475
476 let r = at_rule.parse_next(&mut input);
477 assert_eq!(
478 r,
479 Ok(vec![
480 CssFragment::Class("foo"),
481 CssFragment::Class("bar"),
482 CssFragment::Class("baz")
483 ])
484 );
485
486 assert!(input.is_empty());
487}
488
489#[test]
490fn test_top_level() {
491 let mut input = "// tool.module.scss
492
493 .default_border {
494 border-color: lch(100% 10 10);
495 border-style: dashed double;
496 border-radius: 30px;
497
498 }
499
500 @media testing {
501 .media-foo {
502 color: red;
503 }
504 }
505
506 @layer {
507 .layer-foo {
508 color: blue;
509 }
510 }
511
512 @include mixin {
513 border: none;
514
515 .include-foo {
516 color: green;
517 }
518 }
519
520 @layer foo;
521
522 @debug 1+2 * 3==1+(2 * 3); // true
523
524 .container {
525 padding: 1em;
526 border: 2px solid;
527 border-color: lch(100% 10 10);
528 border-style: dashed double;
529 border-radius: 30px;
530 margin: 1em;
531 background-color: lch(45% 9.5 140.4);
532
533 .bar {
534 color: red;
535 }
536 }
537
538 @debug 1+2 * 3==1+(2 * 3); // true
539 ";
540
541 let r = style_rule_block_contents.parse_next(&mut input);
542 assert_eq!(
543 r,
544 Ok(vec![
545 CssFragment::Class("default_border"),
546 CssFragment::Class("media-foo"),
547 CssFragment::Class("layer-foo"),
548 CssFragment::Class("include-foo"),
549 CssFragment::Class("container"),
550 CssFragment::Class("bar"),
551 ])
552 );
553
554 println!("{input}");
555 assert!(input.is_empty());
556}
557
558#[test]
559fn test_sass_interpolation() {
560 let mut input = "#{$test-test}END";
561
562 let r = sass_interpolation.parse_next(&mut input);
563 assert_eq!(r, Ok("#{$test-test}"));
564
565 assert_eq!(input, "END");
566
567 let mut input = "#{$test-test
568 }END";
569 let r = sass_interpolation.parse_next(&mut input);
570 assert!(r.is_err());
571
572 let mut input = "#{$test-test";
573 let r = sass_interpolation.parse_next(&mut input);
574 assert!(r.is_err());
575
576 let mut input = "#{$test-te{st}";
577 let r = sass_interpolation.parse_next(&mut input);
578 assert!(r.is_err());
579}
580
581#[test]
582fn test_get_class_mappings() {
583 let css = r#".foo.bar {
584 background-color: red;
585 :global(.baz) {
586 color: blue;
587 }
588 :global(.bag .biz) {
589 color: blue;
590 }
591 .zig {
592
593 }
594 .bong {}
595 .zig {
596 color: blue;
597 }
598 }"#;
599 let hash = "abc1234";
600 let mappings = get_class_mappings(css, hash).unwrap();
601 let expected = [
602 ("bag", "bag"),
603 ("bar", "bar-abc1234"),
604 ("baz", "baz"),
605 ("biz", "biz"),
606 ("bong", "bong-abc1234"),
607 ("foo", "foo-abc1234"),
608 ("zig", "zig-abc1234"),
609 ];
610 if mappings.len() != expected.len() {
611 panic!(
612 "Expected {} mappings, got {}",
613 expected.len(),
614 mappings.len()
615 );
616 }
617 for (i, (original, hashed)) in mappings.iter().enumerate() {
618 assert_eq!(expected[i].0, *original);
619 assert_eq!(expected[i].1, *hashed);
620 }
621}
622
623#[test]
624fn test_parser_error_on_nested_globals() {
625 let css = r#".foo :global(.bar .baz) {
626 color: blue;
627 }"#;
628 let result = parse_css(css);
629 assert!(result.is_ok());
630 let css = r#".foo :global(.bar :global(.baz)) {
631 color: blue;
632 }"#;
633 let result = parse_css(css);
634 assert!(result.is_err());
635}
636
637#[test]
638#[should_panic]
639fn test_resolve_global_inner_classes_nested() {
640 let global = Global {
641 inner: ".foo :global(.bar)",
642 outer: ":global(.foo :global(.bar))",
643 };
644 let _ = resolve_global_inner_classes(global);
645}