1use std::collections::HashMap;
7use regex::Regex;
8use thiserror::Error;
9
10pub struct Autoprefixer {
12 browser_data: BrowserData,
13 caniuse_data: CanIUseData,
14 config: AutoprefixerConfig,
15 cache: PrefixCache,
16}
17
18impl Autoprefixer {
19 pub fn new() -> Self {
21 Self::with_config(AutoprefixerConfig::default())
22 }
23
24 pub fn with_config(config: AutoprefixerConfig) -> Self {
26 Self {
27 browser_data: BrowserData::new(),
28 caniuse_data: CanIUseData::new(),
29 config,
30 cache: PrefixCache::new(),
31 }
32 }
33
34 pub fn add_prefixes(&self, css: &str, browsers: &[String]) -> Result<String, AutoprefixerError> {
36 let mut result = String::new();
37 let mut in_rule = false;
38 let mut current_rule = String::new();
39
40 for line in css.lines() {
41 if line.trim().ends_with('{') {
42 in_rule = true;
43 current_rule = line.to_string();
44 } else if in_rule && line.trim() == "}" {
45 current_rule.push_str(line);
46
47 let prefixed_rule = self.prefix_rule(¤t_rule, browsers)?;
48 result.push_str(&prefixed_rule);
49 result.push('\n');
50
51 in_rule = false;
52 current_rule.clear();
53 } else if in_rule {
54 current_rule.push_str(line);
55 current_rule.push('\n');
56 } else {
57 result.push_str(line);
58 result.push('\n');
59 }
60 }
61
62 Ok(result)
63 }
64
65 pub fn add_prefixes_advanced(&self, css: &str, options: &PrefixOptions) -> Result<PrefixResult, AutoprefixerError> {
67 let start_time = std::time::Instant::now();
68 let original_size = css.len();
69
70 let prefixed_css = self.add_prefixes(css, &options.browsers)?;
71 let prefixed_size = prefixed_css.len();
72
73 let prefixes_added = self.count_prefixes_added(&css, &prefixed_css);
74 let prefixes_removed = self.count_prefixes_removed(&css, &prefixed_css);
75 let properties_processed = self.count_properties_processed(&css);
76
77 let processing_time = start_time.elapsed().as_millis() as usize;
78
79 Ok(PrefixResult {
80 prefixed_css,
81 prefixes_added: HashMap::new(),
82 prefixes_removed: HashMap::new(),
83 statistics: PrefixStatistics {
84 original_size,
85 prefixed_size,
86 prefixes_added,
87 prefixes_removed,
88 properties_processed,
89 processing_time_ms: processing_time,
90 },
91 })
92 }
93
94 pub fn needs_prefix(&self, property: &str, browsers: &[String]) -> bool {
96 self.check_browser_compatibility(property, browsers)
97 }
98
99 fn prefix_rule(&self, rule: &str, browsers: &[String]) -> Result<String, AutoprefixerError> {
101 let mut prefixed_rule = String::new();
102 let mut in_declaration = false;
103 let mut current_declaration = String::new();
104
105 for line in rule.lines() {
106 if line.trim().ends_with('{') {
107 prefixed_rule.push_str(line);
108 prefixed_rule.push('\n');
109 } else if line.trim() == "}" {
110 if in_declaration {
111 let prefixed_declaration = self.prefix_declaration(¤t_declaration, browsers)?;
112 prefixed_rule.push_str(&prefixed_declaration);
113 in_declaration = false;
114 current_declaration.clear();
115 }
116 prefixed_rule.push_str(line);
117 prefixed_rule.push('\n');
118 } else if line.trim().ends_with(';') {
119 current_declaration.push_str(line);
120 let prefixed_declaration = self.prefix_declaration(¤t_declaration, browsers)?;
121 prefixed_rule.push_str(&prefixed_declaration);
122 current_declaration.clear();
123 } else {
124 current_declaration.push_str(line);
125 current_declaration.push('\n');
126 in_declaration = true;
127 }
128 }
129
130 Ok(prefixed_rule)
131 }
132
133 fn prefix_declaration(&self, declaration: &str, browsers: &[String]) -> Result<String, AutoprefixerError> {
135 let property_pattern = Regex::new(r"([a-zA-Z-]+)\s*:\s*([^;]+);").unwrap();
136
137 if let Some(cap) = property_pattern.captures(declaration) {
138 let property_name = &cap[1];
139 let property_value = &cap[2];
140
141 if self.needs_prefix(property_name, browsers) {
142 let prefixes = self.get_prefixes_for_property(property_name);
143 let mut prefixed_declaration = String::new();
144
145 for prefix in prefixes {
146 let prefixed_name = format!("{}{}", prefix, property_name);
147 prefixed_declaration.push_str(&format!("{}: {};\n", prefixed_name, property_value));
148 }
149
150 prefixed_declaration.push_str(&format!("{}: {};\n", property_name, property_value));
152
153 Ok(prefixed_declaration)
154 } else {
155 Ok(declaration.to_string())
156 }
157 } else {
158 Ok(declaration.to_string())
159 }
160 }
161
162 fn get_prefixes_for_property(&self, property: &str) -> Vec<String> {
164 let mut prefixes = Vec::new();
165
166 if self.needs_webkit_prefix(property) {
168 prefixes.push("-webkit-".to_string());
169 }
170 if self.needs_moz_prefix(property) {
171 prefixes.push("-moz-".to_string());
172 }
173 if self.needs_ms_prefix(property) {
174 prefixes.push("-ms-".to_string());
175 }
176 if self.needs_o_prefix(property) {
177 prefixes.push("-o-".to_string());
178 }
179
180 prefixes
181 }
182
183 fn needs_webkit_prefix(&self, property: &str) -> bool {
185 matches!(property,
186 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
187 "justify-content" | "align-items" | "align-content" | "align-self" |
188 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
189 "transform" | "transform-origin" | "transform-style" |
190 "transition" | "transition-property" | "transition-duration" |
191 "transition-timing-function" | "transition-delay" |
192 "animation" | "animation-name" | "animation-duration" |
193 "animation-timing-function" | "animation-delay" |
194 "animation-iteration-count" | "animation-direction" |
195 "animation-fill-mode" | "animation-play-state" |
196 "filter" | "backdrop-filter" | "mask" | "mask-image" |
197 "mask-size" | "mask-position" | "mask-repeat" |
198 "mask-origin" | "mask-clip" | "mask-composite" |
199 "clip-path" | "shape-outside" | "shape-margin" |
200 "text-decoration" | "text-decoration-line" |
201 "text-decoration-style" | "text-decoration-color" |
202 "text-decoration-skip" | "text-underline-position" |
203 "text-emphasis" | "text-emphasis-style" |
204 "text-emphasis-color" | "text-emphasis-position" |
205 "text-shadow" | "box-shadow" | "border-radius" |
206 "border-image" | "border-image-source" |
207 "border-image-slice" | "border-image-width" |
208 "border-image-outset" | "border-image-repeat" |
209 "background" | "background-image" | "background-size" |
210 "background-position" | "background-repeat" |
211 "background-attachment" | "background-clip" |
212 "background-origin" | "background-color"
213 )
214 }
215
216 fn needs_moz_prefix(&self, property: &str) -> bool {
218 matches!(property,
219 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
220 "justify-content" | "align-items" | "align-content" | "align-self" |
221 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
222 "transform" | "transform-origin" | "transform-style" |
223 "transition" | "transition-property" | "transition-duration" |
224 "transition-timing-function" | "transition-delay" |
225 "animation" | "animation-name" | "animation-duration" |
226 "animation-timing-function" | "animation-delay" |
227 "animation-iteration-count" | "animation-direction" |
228 "animation-fill-mode" | "animation-play-state" |
229 "filter" | "backdrop-filter" | "mask" | "mask-image" |
230 "mask-size" | "mask-position" | "mask-repeat" |
231 "mask-origin" | "mask-clip" | "mask-composite" |
232 "clip-path" | "shape-outside" | "shape-margin" |
233 "text-decoration" | "text-decoration-line" |
234 "text-decoration-style" | "text-decoration-color" |
235 "text-decoration-skip" | "text-underline-position" |
236 "text-emphasis" | "text-emphasis-style" |
237 "text-emphasis-color" | "text-emphasis-position" |
238 "text-shadow" | "box-shadow" | "border-radius" |
239 "border-image" | "border-image-source" |
240 "border-image-slice" | "border-image-width" |
241 "border-image-outset" | "border-image-repeat" |
242 "background" | "background-image" | "background-size" |
243 "background-position" | "background-repeat" |
244 "background-attachment" | "background-clip" |
245 "background-origin" | "background-color"
246 )
247 }
248
249 fn needs_ms_prefix(&self, property: &str) -> bool {
251 matches!(property,
252 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
253 "justify-content" | "align-items" | "align-content" | "align-self" |
254 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
255 "transform" | "transform-origin" | "transform-style" |
256 "transition" | "transition-property" | "transition-duration" |
257 "transition-timing-function" | "transition-delay" |
258 "animation" | "animation-name" | "animation-duration" |
259 "animation-timing-function" | "animation-delay" |
260 "animation-iteration-count" | "animation-direction" |
261 "animation-fill-mode" | "animation-play-state" |
262 "filter" | "backdrop-filter" | "mask" | "mask-image" |
263 "mask-size" | "mask-position" | "mask-repeat" |
264 "mask-origin" | "mask-clip" | "mask-composite" |
265 "clip-path" | "shape-outside" | "shape-margin" |
266 "text-decoration" | "text-decoration-line" |
267 "text-decoration-style" | "text-decoration-color" |
268 "text-decoration-skip" | "text-underline-position" |
269 "text-emphasis" | "text-emphasis-style" |
270 "text-emphasis-color" | "text-emphasis-position" |
271 "text-shadow" | "box-shadow" | "border-radius" |
272 "border-image" | "border-image-source" |
273 "border-image-slice" | "border-image-width" |
274 "border-image-outset" | "border-image-repeat" |
275 "background" | "background-image" | "background-size" |
276 "background-position" | "background-repeat" |
277 "background-attachment" | "background-clip" |
278 "background-origin" | "background-color"
279 )
280 }
281
282 fn needs_o_prefix(&self, property: &str) -> bool {
284 matches!(property,
285 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
286 "justify-content" | "align-items" | "align-content" | "align-self" |
287 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
288 "transform" | "transform-origin" | "transform-style" |
289 "transition" | "transition-property" | "transition-duration" |
290 "transition-timing-function" | "transition-delay" |
291 "animation" | "animation-name" | "animation-duration" |
292 "animation-timing-function" | "animation-delay" |
293 "animation-iteration-count" | "animation-direction" |
294 "animation-fill-mode" | "animation-play-state" |
295 "filter" | "backdrop-filter" | "mask" | "mask-image" |
296 "mask-size" | "mask-position" | "mask-repeat" |
297 "mask-origin" | "mask-clip" | "mask-composite" |
298 "clip-path" | "shape-outside" | "shape-margin" |
299 "text-decoration" | "text-decoration-line" |
300 "text-decoration-style" | "text-decoration-color" |
301 "text-decoration-skip" | "text-underline-position" |
302 "text-emphasis" | "text-emphasis-style" |
303 "text-emphasis-color" | "text-emphasis-position" |
304 "text-shadow" | "box-shadow" | "border-radius" |
305 "border-image" | "border-image-source" |
306 "border-image-slice" | "border-image-width" |
307 "border-image-outset" | "border-image-repeat" |
308 "background" | "background-image" | "background-size" |
309 "background-position" | "background-repeat" |
310 "background-attachment" | "background-clip" |
311 "background-origin" | "background-color"
312 )
313 }
314
315 fn check_browser_compatibility(&self, property: &str, browsers: &[String]) -> bool {
317 for browser in browsers {
318 let support = self.browser_data.get_feature_support(property, browser);
319 match support {
320 Some(SupportLevel::Full) => continue,
321 Some(SupportLevel::Partial) => return true, Some(SupportLevel::None) => return true, Some(SupportLevel::Unknown) => return true, None => return true, }
326 }
327 false
328 }
329
330 fn count_prefixes_added(&self, original: &str, prefixed: &str) -> usize {
332 let original_prefixes = self.count_prefixes_in_css(original);
333 let prefixed_prefixes = self.count_prefixes_in_css(prefixed);
334 prefixed_prefixes - original_prefixes
335 }
336
337 fn count_prefixes_removed(&self, original: &str, prefixed: &str) -> usize {
339 let original_prefixes = self.count_prefixes_in_css(original);
340 let prefixed_prefixes = self.count_prefixes_in_css(prefixed);
341 if original_prefixes > prefixed_prefixes {
342 original_prefixes - prefixed_prefixes
343 } else {
344 0
345 }
346 }
347
348 fn count_prefixes_in_css(&self, css: &str) -> usize {
350 let prefix_patterns = ["-webkit-", "-moz-", "-ms-", "-o-"];
351 let mut count = 0;
352
353 for pattern in &prefix_patterns {
354 count += css.matches(pattern).count();
355 }
356
357 count
358 }
359
360 fn count_properties_processed(&self, css: &str) -> usize {
362 let property_pattern = Regex::new(r"([a-zA-Z-]+)\s*:\s*([^;]+);").unwrap();
363 property_pattern.captures_iter(css).count()
364 }
365}
366
367pub struct BrowserData {
369 browsers: HashMap<String, BrowserInfo>,
370 features: HashMap<String, FeatureSupport>,
371 versions: HashMap<String, Vec<String>>,
372}
373
374impl BrowserData {
375 pub fn new() -> Self {
377 let mut browsers = HashMap::new();
378 let features = HashMap::new();
379 let mut versions = HashMap::new();
380
381 browsers.insert("chrome".to_string(), BrowserInfo {
383 name: "Chrome".to_string(),
384 versions: vec!["30".to_string(), "40".to_string(), "50".to_string(), "60".to_string(), "70".to_string(), "80".to_string(), "90".to_string(), "100".to_string()],
385 current_version: "100".to_string(),
386 support_level: SupportLevel::Full,
387 });
388
389 browsers.insert("firefox".to_string(), BrowserInfo {
390 name: "Firefox".to_string(),
391 versions: vec!["25".to_string(), "30".to_string(), "40".to_string(), "50".to_string(), "60".to_string(), "70".to_string(), "80".to_string(), "90".to_string()],
392 current_version: "90".to_string(),
393 support_level: SupportLevel::Full,
394 });
395
396 browsers.insert("safari".to_string(), BrowserInfo {
397 name: "Safari".to_string(),
398 versions: vec!["7".to_string(), "8".to_string(), "9".to_string(), "10".to_string(), "11".to_string(), "12".to_string(), "13".to_string(), "14".to_string()],
399 current_version: "14".to_string(),
400 support_level: SupportLevel::Full,
401 });
402
403 browsers.insert("ie".to_string(), BrowserInfo {
404 name: "Internet Explorer".to_string(),
405 versions: vec!["8".to_string(), "9".to_string(), "10".to_string(), "11".to_string()],
406 current_version: "11".to_string(),
407 support_level: SupportLevel::Partial,
408 });
409
410 browsers.insert("edge".to_string(), BrowserInfo {
411 name: "Edge".to_string(),
412 versions: vec!["12".to_string(), "13".to_string(), "14".to_string(), "15".to_string(), "16".to_string(), "17".to_string(), "18".to_string(), "19".to_string()],
413 current_version: "19".to_string(),
414 support_level: SupportLevel::Full,
415 });
416
417 for (browser, info) in &browsers {
419 versions.insert(browser.clone(), info.versions.clone());
420 }
421
422 Self {
423 browsers,
424 features,
425 versions,
426 }
427 }
428
429 pub fn get_feature_support(&self, feature: &str, browser: &str) -> Option<SupportLevel> {
431 match browser {
433 "chrome" | "firefox" | "safari" | "edge" => Some(SupportLevel::Full),
434 "ie" => Some(SupportLevel::Partial),
435 _ => Some(SupportLevel::Unknown),
436 }
437 }
438
439 pub fn supports_feature(&self, _feature: &str, browser: &str, _version: &str) -> bool {
441 match self.get_feature_support(_feature, browser) {
442 Some(SupportLevel::Full) => true,
443 Some(SupportLevel::Partial) => true,
444 Some(SupportLevel::None) => false,
445 Some(SupportLevel::Unknown) => false,
446 None => false,
447 }
448 }
449}
450
451pub struct CanIUseData {
453 features: HashMap<String, FeatureSupport>,
454}
455
456impl CanIUseData {
457 pub fn new() -> Self {
459 Self {
460 features: HashMap::new(),
461 }
462 }
463
464 pub fn get_support(&self, _feature: &str, browser: &str) -> Option<SupportLevel> {
466 match browser {
468 "chrome" | "firefox" | "safari" | "edge" => Some(SupportLevel::Full),
469 "ie" => Some(SupportLevel::Partial),
470 _ => Some(SupportLevel::Unknown),
471 }
472 }
473}
474
475pub struct PrefixGenerator {
477 prefixes: HashMap<String, Vec<String>>,
478 config: GeneratorConfig,
479}
480
481impl PrefixGenerator {
482 pub fn new() -> Self {
484 Self {
485 prefixes: HashMap::new(),
486 config: GeneratorConfig::default(),
487 }
488 }
489
490 pub fn generate_prefixes(&self, property: &str, browsers: &[String]) -> Vec<String> {
492 let mut prefixes = Vec::new();
493
494 for browser in browsers {
495 match browser.as_str() {
496 "chrome" | "safari" => {
497 if self.needs_webkit_prefix(property) {
498 prefixes.push("-webkit-".to_string());
499 }
500 },
501 "firefox" => {
502 if self.needs_moz_prefix(property) {
503 prefixes.push("-moz-".to_string());
504 }
505 },
506 "ie" | "edge" => {
507 if self.needs_ms_prefix(property) {
508 prefixes.push("-ms-".to_string());
509 }
510 },
511 _ => {}
512 }
513 }
514
515 prefixes
516 }
517
518 fn needs_webkit_prefix(&self, property: &str) -> bool {
520 matches!(property,
521 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
522 "justify-content" | "align-items" | "align-content" | "align-self" |
523 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
524 "transform" | "transform-origin" | "transform-style" |
525 "transition" | "transition-property" | "transition-duration" |
526 "transition-timing-function" | "transition-delay" |
527 "animation" | "animation-name" | "animation-duration" |
528 "animation-timing-function" | "animation-delay" |
529 "animation-iteration-count" | "animation-direction" |
530 "animation-fill-mode" | "animation-play-state" |
531 "filter" | "backdrop-filter" | "mask" | "mask-image" |
532 "mask-size" | "mask-position" | "mask-repeat" |
533 "mask-origin" | "mask-clip" | "mask-composite" |
534 "clip-path" | "shape-outside" | "shape-margin" |
535 "text-decoration" | "text-decoration-line" |
536 "text-decoration-style" | "text-decoration-color" |
537 "text-decoration-skip" | "text-underline-position" |
538 "text-emphasis" | "text-emphasis-style" |
539 "text-emphasis-color" | "text-emphasis-position" |
540 "text-shadow" | "box-shadow" | "border-radius" |
541 "border-image" | "border-image-source" |
542 "border-image-slice" | "border-image-width" |
543 "border-image-outset" | "border-image-repeat" |
544 "background" | "background-image" | "background-size" |
545 "background-position" | "background-repeat" |
546 "background-attachment" | "background-clip" |
547 "background-origin" | "background-color"
548 )
549 }
550
551 fn needs_moz_prefix(&self, property: &str) -> bool {
553 matches!(property,
554 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
555 "justify-content" | "align-items" | "align-content" | "align-self" |
556 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
557 "transform" | "transform-origin" | "transform-style" |
558 "transition" | "transition-property" | "transition-duration" |
559 "transition-timing-function" | "transition-delay" |
560 "animation" | "animation-name" | "animation-duration" |
561 "animation-timing-function" | "animation-delay" |
562 "animation-iteration-count" | "animation-direction" |
563 "animation-fill-mode" | "animation-play-state" |
564 "filter" | "backdrop-filter" | "mask" | "mask-image" |
565 "mask-size" | "mask-position" | "mask-repeat" |
566 "mask-origin" | "mask-clip" | "mask-composite" |
567 "clip-path" | "shape-outside" | "shape-margin" |
568 "text-decoration" | "text-decoration-line" |
569 "text-decoration-style" | "text-decoration-color" |
570 "text-decoration-skip" | "text-underline-position" |
571 "text-emphasis" | "text-emphasis-style" |
572 "text-emphasis-color" | "text-emphasis-position" |
573 "text-shadow" | "box-shadow" | "border-radius" |
574 "border-image" | "border-image-source" |
575 "border-image-slice" | "border-image-width" |
576 "border-image-outset" | "border-image-repeat" |
577 "background" | "background-image" | "background-size" |
578 "background-position" | "background-repeat" |
579 "background-attachment" | "background-clip" |
580 "background-origin" | "background-color"
581 )
582 }
583
584 fn needs_ms_prefix(&self, property: &str) -> bool {
586 matches!(property,
587 "display" | "flex" | "flex-direction" | "flex-wrap" | "flex-flow" |
588 "justify-content" | "align-items" | "align-content" | "align-self" |
589 "flex-grow" | "flex-shrink" | "flex-basis" | "order" |
590 "transform" | "transform-origin" | "transform-style" |
591 "transition" | "transition-property" | "transition-duration" |
592 "transition-timing-function" | "transition-delay" |
593 "animation" | "animation-name" | "animation-duration" |
594 "animation-timing-function" | "animation-delay" |
595 "animation-iteration-count" | "animation-direction" |
596 "animation-fill-mode" | "animation-play-state" |
597 "filter" | "backdrop-filter" | "mask" | "mask-image" |
598 "mask-size" | "mask-position" | "mask-repeat" |
599 "mask-origin" | "mask-clip" | "mask-composite" |
600 "clip-path" | "shape-outside" | "shape-margin" |
601 "text-decoration" | "text-decoration-line" |
602 "text-decoration-style" | "text-decoration-color" |
603 "text-decoration-skip" | "text-underline-position" |
604 "text-emphasis" | "text-emphasis-style" |
605 "text-emphasis-color" | "text-emphasis-position" |
606 "text-shadow" | "box-shadow" | "border-radius" |
607 "border-image" | "border-image-source" |
608 "border-image-slice" | "border-image-width" |
609 "border-image-outset" | "border-image-repeat" |
610 "background" | "background-image" | "background-size" |
611 "background-position" | "background-repeat" |
612 "background-attachment" | "background-clip" |
613 "background-origin" | "background-color"
614 )
615 }
616}
617
618pub struct PrefixCache {
620 property_cache: HashMap<String, Vec<String>>,
621 browser_cache: HashMap<String, SupportLevel>,
622 css_cache: HashMap<String, String>,
623}
624
625impl PrefixCache {
626 pub fn new() -> Self {
628 Self {
629 property_cache: HashMap::new(),
630 browser_cache: HashMap::new(),
631 css_cache: HashMap::new(),
632 }
633 }
634
635 pub fn get_cached_prefixes(&self, property: &str) -> Option<&Vec<String>> {
637 self.property_cache.get(property)
638 }
639
640 pub fn cache_prefixes(&mut self, property: String, prefixes: Vec<String>) {
642 self.property_cache.insert(property, prefixes);
643 }
644
645 pub fn get_cached_css(&self, css: &str) -> Option<&String> {
647 self.css_cache.get(css)
648 }
649
650 pub fn cache_css(&mut self, css: String, prefixed: String) {
652 self.css_cache.insert(css, prefixed);
653 }
654}
655
656#[derive(Debug, Clone)]
658pub struct AutoprefixerConfig {
659 pub browsers: Vec<String>,
660 pub cascade: bool,
661 pub add: bool,
662 pub remove: bool,
663 pub supports: bool,
664 pub flexbox: FlexboxMode,
665 pub grid: GridMode,
666 pub stats: Option<BrowserStats>,
667}
668
669impl Default for AutoprefixerConfig {
670 fn default() -> Self {
671 Self {
672 browsers: vec![
673 "chrome 30".to_string(),
674 "firefox 25".to_string(),
675 "safari 7".to_string(),
676 "ie 10".to_string(),
677 "edge 12".to_string(),
678 ],
679 cascade: true,
680 add: true,
681 remove: false,
682 supports: true,
683 flexbox: FlexboxMode::All,
684 grid: GridMode::Autoplace,
685 stats: None,
686 }
687 }
688}
689
690#[derive(Debug, Clone)]
692pub enum FlexboxMode {
693 No2009,
694 No2012,
695 All,
696}
697
698#[derive(Debug, Clone)]
700pub enum GridMode {
701 NoAutoplace,
702 Autoplace,
703}
704
705#[derive(Debug, Clone)]
707pub struct PrefixOptions {
708 pub browsers: Vec<String>,
709 pub cascade: bool,
710 pub add: bool,
711 pub remove: bool,
712 pub supports: bool,
713 pub flexbox: FlexboxMode,
714 pub grid: GridMode,
715 pub stats: Option<BrowserStats>,
716}
717
718#[derive(Debug, Clone)]
720pub struct PrefixResult {
721 pub prefixed_css: String,
722 pub prefixes_added: HashMap<String, Vec<String>>,
723 pub prefixes_removed: HashMap<String, Vec<String>>,
724 pub statistics: PrefixStatistics,
725}
726
727#[derive(Debug, Clone)]
729pub struct PrefixStatistics {
730 pub original_size: usize,
731 pub prefixed_size: usize,
732 pub prefixes_added: usize,
733 pub prefixes_removed: usize,
734 pub properties_processed: usize,
735 pub processing_time_ms: usize,
736}
737
738#[derive(Debug, Clone)]
740pub struct BrowserInfo {
741 pub name: String,
742 pub versions: Vec<String>,
743 pub current_version: String,
744 pub support_level: SupportLevel,
745}
746
747#[derive(Debug, Clone)]
749pub enum SupportLevel {
750 Full,
751 Partial,
752 None,
753 Unknown,
754}
755
756#[derive(Debug, Clone)]
758pub struct FeatureSupport {
759 pub feature: String,
760 pub browsers: HashMap<String, SupportInfo>,
761}
762
763#[derive(Debug, Clone)]
765pub struct SupportInfo {
766 pub version: String,
767 pub support: SupportLevel,
768 pub prefix: Option<String>,
769 pub notes: Option<String>,
770}
771
772#[derive(Debug, Clone)]
774pub struct GeneratorConfig {
775 pub optimize: bool,
776 pub remove_unused: bool,
777 pub add_missing: bool,
778}
779
780impl Default for GeneratorConfig {
781 fn default() -> Self {
782 Self {
783 optimize: true,
784 remove_unused: false,
785 add_missing: true,
786 }
787 }
788}
789
790#[derive(Debug, Clone)]
792pub struct BrowserStats {
793 pub usage: HashMap<String, f64>,
794}
795
796#[derive(Debug, Error)]
798pub enum AutoprefixerError {
799 #[error("Invalid CSS syntax: {error}")]
800 InvalidCSS { error: String },
801
802 #[error("Unsupported browser: {browser}")]
803 UnsupportedBrowser { browser: String },
804
805 #[error("Failed to load browser data: {error}")]
806 BrowserDataError { error: String },
807
808 #[error("Prefix generation failed: {property}")]
809 PrefixGenerationError { property: String },
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
817 fn test_basic_prefixing() {
818 let autoprefixer = Autoprefixer::new();
819 let css = ".test { display: flex; }";
820 let result = autoprefixer.add_prefixes(css, &["chrome 30".to_string()]);
821 assert!(result.is_ok());
822 let prefixed = result.unwrap();
823 assert!(prefixed.contains("-webkit-"));
824 }
825
826 #[test]
827 fn test_flexbox_prefixing() {
828 let mut config = AutoprefixerConfig::default();
829 config.flexbox = FlexboxMode::All;
830 let autoprefixer = Autoprefixer::with_config(config);
831
832 let css = ".container { display: flex; }";
833 let result = autoprefixer.add_prefixes(css, &["ie 10".to_string()]);
834 assert!(result.is_ok());
835 let prefixed = result.unwrap();
836 assert!(prefixed.contains("-ms-"));
837 }
838
839 #[test]
840 fn test_complex_prefixing() {
841 let autoprefixer = Autoprefixer::new();
842
843 let css = r#"
844 .container {
845 display: flex;
846 flex-direction: column;
847 align-items: center;
848 }
849
850 .item {
851 flex: 1;
852 transform: translateX(10px);
853 }
854 "#;
855
856 let browsers = vec![
857 "chrome 30".to_string(),
858 "firefox 25".to_string(),
859 "safari 7".to_string(),
860 ];
861
862 let result = autoprefixer.add_prefixes(css, &browsers);
863 assert!(result.is_ok());
864
865 let prefixed = result.unwrap();
866 assert!(prefixed.contains("-webkit-"));
867 assert!(prefixed.contains("-moz-"));
868 }
869
870 #[test]
871 fn test_prefix_generator() {
872 let generator = PrefixGenerator::new();
873 let prefixes = generator.generate_prefixes("display", &["chrome".to_string(), "firefox".to_string()]);
874 assert!(!prefixes.is_empty());
875 }
876
877 #[test]
878 fn test_browser_data() {
879 let browser_data = BrowserData::new();
880 let support = browser_data.get_feature_support("display", "chrome");
881 assert!(support.is_some());
882 }
883
884 #[test]
885 fn test_prefix_cache() {
886 let mut cache = PrefixCache::new();
887 let prefixes = vec!["-webkit-".to_string(), "-moz-".to_string()];
888 cache.cache_prefixes("display".to_string(), prefixes);
889
890 let cached = cache.get_cached_prefixes("display");
891 assert!(cached.is_some());
892 assert_eq!(cached.unwrap().len(), 2);
893 }
894}