@prospective.co/procss 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +45 -0
  3. package/package.json +31 -0
  4. package/src/ast/flat_ruleset.rs +52 -0
  5. package/src/ast/ruleset/mod.rs +229 -0
  6. package/src/ast/ruleset/rule.rs +114 -0
  7. package/src/ast/selector/attribute.rs +90 -0
  8. package/src/ast/selector/combinator.rs +58 -0
  9. package/src/ast/selector/mod.rs +115 -0
  10. package/src/ast/selector/selector_path.rs +230 -0
  11. package/src/ast/selector/selector_term.rs +325 -0
  12. package/src/ast/token/mod.rs +18 -0
  13. package/src/ast/token/space.rs +157 -0
  14. package/src/ast/token/string.rs +59 -0
  15. package/src/ast/token/symbol.rs +32 -0
  16. package/src/ast/tree_ruleset.rs +252 -0
  17. package/src/ast.rs +216 -0
  18. package/src/builder.rs +189 -0
  19. package/src/js_builder.rs +59 -0
  20. package/src/lib.rs +151 -0
  21. package/src/main.rs +40 -0
  22. package/src/parser.rs +36 -0
  23. package/src/render.rs +84 -0
  24. package/src/transform.rs +14 -0
  25. package/src/transformers/apply_import.rs +53 -0
  26. package/src/transformers/apply_mixin.rs +73 -0
  27. package/src/transformers/apply_var.rs +41 -0
  28. package/src/transformers/dedupe.rs +35 -0
  29. package/src/transformers/filter_refs.rs +27 -0
  30. package/src/transformers/flat_self.rs +30 -0
  31. package/src/transformers/inline_url.rs +52 -0
  32. package/src/transformers/mod.rs +43 -0
  33. package/src/utils.rs +90 -0
  34. package/target/cjs/procss.d.ts +22 -0
  35. package/target/cjs/procss.js +217 -0
  36. package/target/cjs/procss_bg.wasm +0 -0
  37. package/target/cjs/procss_bg.wasm.d.ts +12 -0
  38. package/target/esm/procss.d.ts +58 -0
  39. package/target/esm/procss.js +283 -0
  40. package/target/esm/procss_bg.wasm +0 -0
  41. package/target/esm/procss_bg.wasm.d.ts +12 -0
@@ -0,0 +1,252 @@
1
+ // ┌───────────────────────────────────────────────────────────────────────────┐
2
+ // │ │
3
+ // │ ██████╗ ██████╗ ██████╗ Copyright (C) 2022, The Prospective Company │
4
+ // │ ██╔══██╗██╔══██╗██╔═══██╗ │
5
+ // │ ██████╔╝██████╔╝██║ ██║ This file is part of the Procss library, │
6
+ // │ ██╔═══╝ ██╔══██╗██║ ██║ distributed under the terms of the │
7
+ // │ ██║ ██║ ██║╚██████╔╝ Apache License 2.0. The full license can │
8
+ // │ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ be found in the LICENSE file. │
9
+ // │ │
10
+ // └───────────────────────────────────────────────────────────────────────────┘
11
+
12
+ use nom::branch::alt;
13
+ use nom::bytes::complete::tag;
14
+ use nom::combinator::peek;
15
+ use nom::error::ParseError;
16
+ use nom::multi::{many0, many1};
17
+ use nom::sequence::{terminated, tuple};
18
+ use nom::{IResult, Parser};
19
+
20
+ use super::flat_ruleset::FlatRuleset;
21
+ use super::ruleset::{QualNestedRuleset, QualRule, QualRuleset, Rule, Ruleset, SelectorRuleset};
22
+ use super::selector::Selector;
23
+ use super::token::{comment0, sep0};
24
+ use crate::parser::*;
25
+ use crate::render::*;
26
+ use crate::transform::TransformCss;
27
+
28
+ /// A tree node which expresses a recursive `T` over `Ruleset<T>`. Using this
29
+ /// struct in place of `Rule` allows nested CSS selectors that can be later
30
+ /// flattened.
31
+ #[derive(Clone, Debug)]
32
+ pub enum TreeRule<'a> {
33
+ Rule(Rule<'a>),
34
+ Ruleset(TreeRuleset<'a>),
35
+ }
36
+
37
+ impl<'a> ParseCss<'a> for TreeRule<'a> {
38
+ fn parse<E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Self, E> {
39
+ let block = terminated(TreeRuleset::parse, sep0).map(TreeRule::Ruleset);
40
+ let rule = terminated(Rule::parse, sep0).map(TreeRule::Rule);
41
+ alt((block, rule))(input)
42
+ }
43
+ }
44
+
45
+ impl<'a> RenderCss for TreeRule<'a> {
46
+ fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47
+ match self {
48
+ TreeRule::Rule(rule) => rule.render(f),
49
+ TreeRule::Ruleset(block) => block.render(f),
50
+ }
51
+ }
52
+ }
53
+
54
+ impl<'a> TransformCss<Rule<'a>> for TreeRule<'a> {
55
+ fn transform_each<F: FnMut(&mut Rule<'a>)>(&mut self, f: &mut F) {
56
+ match self {
57
+ TreeRule::Rule(rule) => f(rule),
58
+ TreeRule::Ruleset(ruleset) => ruleset.transform_each(f),
59
+ }
60
+ }
61
+ }
62
+
63
+ impl<'a> TransformCss<TreeRuleset<'a>> for TreeRule<'a> {
64
+ fn transform_each<F: FnMut(&mut TreeRuleset<'a>)>(&mut self, f: &mut F) {
65
+ match self {
66
+ TreeRule::Rule(_) => (),
67
+ TreeRule::Ruleset(ruleset) => ruleset.transform_each(f),
68
+ }
69
+ }
70
+ }
71
+
72
+ impl<'a> TransformCss<Vec<TreeRuleset<'a>>> for TreeRule<'a> {
73
+ fn transform_each<F: FnMut(&mut Vec<TreeRuleset<'a>>)>(&mut self, f: &mut F) {
74
+ match self {
75
+ TreeRule::Rule(_) => (),
76
+ TreeRule::Ruleset(ruleset) => ruleset.transform_each(f),
77
+ }
78
+ }
79
+ }
80
+
81
+ /// A nested recursive block, ala popular CSS tools and the CSS nesting
82
+ /// proposal.
83
+ ///
84
+ /// ```css
85
+ /// div {
86
+ /// color: green;
87
+ /// &#my_elem {
88
+ /// color: red;
89
+ /// }
90
+ /// .sub_elem {
91
+ /// color: purple;
92
+ /// }
93
+ /// }
94
+ /// ```
95
+ pub type TreeRuleset<'a> = Ruleset<'a, TreeRule<'a>>;
96
+
97
+ impl<'a> ParseCss<'a> for TreeRuleset<'a> {
98
+ fn parse<E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Self, E> {
99
+ if let Ok((input, _)) = peek::<_, _, E, _>(tag("@"))(input) {
100
+ let (input, qual_rule) = QualRule::parse(input)?;
101
+ if let Ok((input, _)) = tag::<_, _, E>(";")(input) {
102
+ Ok((input, Ruleset::QualRule(qual_rule)))
103
+ } else {
104
+ let (input, _) = tuple((tag("{"), sep0))(input)?;
105
+ let (input, rules) = many1(TreeRule::parse::<E>)(input)?;
106
+ let (input, _) = tuple((comment0, tag("}")))(input)?;
107
+ Ok((input, Ruleset::QualRuleset(QualRuleset(qual_rule, rules))))
108
+ }
109
+ } else {
110
+ let (input, selector_ruleset) = SelectorRuleset::parse(input)?;
111
+ Ok((input, Ruleset::SelectorRuleset(selector_ruleset)))
112
+ }
113
+ }
114
+ }
115
+
116
+ impl<'a> TransformCss<TreeRuleset<'a>> for TreeRuleset<'a> {
117
+ fn transform_each<F: FnMut(&mut TreeRuleset<'a>)>(&mut self, f: &mut F) {
118
+ f(self);
119
+ match self {
120
+ Ruleset::QualRule(_) => (),
121
+ Ruleset::QualRuleset(_) => (),
122
+ Ruleset::QualNestedRuleset(..) => (),
123
+ Ruleset::SelectorRuleset(ruleset) => {
124
+ for rule in ruleset.1.iter_mut() {
125
+ rule.transform_each(f)
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ impl<'a> TreeRuleset<'a> {
133
+ /// Flatten into a `FlatRuleset`, replacing this struct's inner `TreeRule`
134
+ /// recursive type with a regular `Rule`, removing arbitrary nesting of
135
+ /// `SelectorRuleset` variants.
136
+ pub fn flatten_tree(&self) -> Vec<FlatRuleset<'a>> {
137
+ match self {
138
+ Ruleset::SelectorRuleset(ruleset) => ruleset.flatten_tree(),
139
+ Ruleset::QualRule(x) => vec![Ruleset::QualRule(x.clone())],
140
+ Ruleset::QualRuleset(rules) => {
141
+ let mut new_rules: Vec<Rule<'a>> = vec![];
142
+ let mut new_rulesets: Vec<FlatRuleset<'a>> = vec![];
143
+ for rule in rules.1.iter() {
144
+ match rule {
145
+ TreeRule::Rule(rule) => new_rules.push(rule.clone()),
146
+ TreeRule::Ruleset(ruleset) => {
147
+ let sub_rules = ruleset.flatten_tree().into_iter();
148
+ new_rulesets.extend(sub_rules)
149
+ }
150
+ }
151
+ }
152
+
153
+ let mut ret = vec![];
154
+ if !new_rules.is_empty() {
155
+ let ruleset = QualRuleset(rules.0.clone(), new_rules);
156
+ ret.push(Ruleset::QualRuleset(ruleset));
157
+ }
158
+
159
+ if !new_rulesets.is_empty() {
160
+ ret.push(Ruleset::QualNestedRuleset(QualNestedRuleset(
161
+ rules.0.clone(),
162
+ new_rulesets,
163
+ )))
164
+ }
165
+
166
+ ret
167
+ }
168
+ Ruleset::QualNestedRuleset(ruleset) => {
169
+ vec![Ruleset::QualNestedRuleset(QualNestedRuleset(
170
+ ruleset.0.clone(),
171
+ ruleset.1.iter().flat_map(|x| x.flatten_tree()).collect(),
172
+ ))]
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ type TreeSelectorRuleset<'a> = SelectorRuleset<'a, TreeRule<'a>>;
179
+
180
+ impl<'a> ParseCss<'a> for TreeSelectorRuleset<'a> {
181
+ fn parse<E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Self, E> {
182
+ let (input, selector) = Selector::parse(input)?;
183
+ let (input, _) = tuple((comment0, tag("{"), sep0))(input)?;
184
+ let (input, rules) = many0(TreeRule::parse)(input)?;
185
+ let (input, _) = tuple((comment0, tag("}")))(input)?;
186
+ Ok((input, SelectorRuleset(selector, rules)))
187
+ }
188
+ }
189
+
190
+ impl<'a> TransformCss<Vec<TreeRuleset<'a>>> for TreeRuleset<'a> {
191
+ fn transform_each<F: FnMut(&mut Vec<TreeRuleset<'a>>)>(&mut self, f: &mut F) {
192
+ match self {
193
+ Ruleset::SelectorRuleset(ruleset) => {
194
+ for rule in ruleset.1.iter_mut() {
195
+ rule.transform_each(f)
196
+ }
197
+ }
198
+ Ruleset::QualRule(_) => (),
199
+ Ruleset::QualRuleset(_) => (),
200
+ Ruleset::QualNestedRuleset(ruleset) => {
201
+ for rule in ruleset.1.iter_mut() {
202
+ rule.transform_each(f)
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ impl<'a> TreeSelectorRuleset<'a> {
210
+ /// Flatten a TreeRuleset's SelectorRuleset into a `FlatRuleset`m erging any
211
+ /// nested rulesets which are not allowed in the latter.
212
+ pub fn flatten_tree(&self) -> Vec<Ruleset<'a, Rule<'a>>> {
213
+ let mut new_rules: Vec<Rule<'a>> = vec![];
214
+ let mut new_rulesets: Vec<FlatRuleset<'a>> = vec![];
215
+ for rule in self.1.iter() {
216
+ match rule {
217
+ TreeRule::Rule(rule) => new_rules.push(rule.clone()),
218
+ TreeRule::Ruleset(ruleset) => {
219
+ if !new_rules.is_empty() {
220
+ let ruleset = SelectorRuleset(self.0.clone(), new_rules);
221
+ new_rulesets.push(Ruleset::SelectorRuleset(ruleset));
222
+ new_rules = vec![];
223
+ }
224
+
225
+ let sub_rules = ruleset
226
+ .flatten_tree()
227
+ .into_iter()
228
+ .map(|ruleset| self.join(ruleset));
229
+ new_rulesets.extend(sub_rules)
230
+ }
231
+ }
232
+ }
233
+
234
+ if !new_rules.is_empty() {
235
+ let ruleset = SelectorRuleset(self.0.clone(), new_rules);
236
+ new_rulesets.push(Ruleset::SelectorRuleset(ruleset));
237
+ }
238
+
239
+ new_rulesets
240
+ }
241
+
242
+ /// Join a new `Ruleset` as an extension of self's selector.
243
+ fn join(&self, rhs: Ruleset<'a, Rule<'a>>) -> Ruleset<'a, Rule<'a>> {
244
+ match rhs {
245
+ Ruleset::SelectorRuleset(inner_ruleset) => {
246
+ let joined_selector = self.0.join(&inner_ruleset.0);
247
+ Ruleset::SelectorRuleset(SelectorRuleset(joined_selector, inner_ruleset.1))
248
+ }
249
+ ruleset => ruleset,
250
+ }
251
+ }
252
+ }
package/src/ast.rs ADDED
@@ -0,0 +1,216 @@
1
+ // ┌───────────────────────────────────────────────────────────────────────────┐
2
+ // │ │
3
+ // │ ██████╗ ██████╗ ██████╗ Copyright (C) 2022, The Prospective Company │
4
+ // │ ██╔══██╗██╔══██╗██╔═══██╗ │
5
+ // │ ██████╔╝██████╔╝██║ ██║ This file is part of the Procss library, │
6
+ // │ ██╔═══╝ ██╔══██╗██║ ██║ distributed under the terms of the │
7
+ // │ ██║ ██║ ██║╚██████╔╝ Apache License 2.0. The full license can │
8
+ // │ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ be found in the LICENSE file. │
9
+ // │ │
10
+ // └───────────────────────────────────────────────────────────────────────────┘
11
+
12
+ //! The complete AST structs for [`Css`] and [`Tree`]. These top-level structs
13
+ //! are part of the [`crate::parse`] API, and their various components structs
14
+ //! are necessary for writing any transformations on these between parsing and
15
+ //! rendering.
16
+
17
+ mod flat_ruleset;
18
+ mod ruleset;
19
+ mod selector;
20
+ mod token;
21
+ mod tree_ruleset;
22
+
23
+ use std::cmp::Ordering;
24
+
25
+ use nom::branch::alt;
26
+ use nom::combinator::eof;
27
+ use nom::error::ParseError;
28
+ use nom::multi::many1;
29
+ use nom::sequence::terminated;
30
+ use nom::{IResult, Parser};
31
+
32
+ pub use self::flat_ruleset::FlatRuleset;
33
+ pub use self::ruleset::{QualNestedRuleset, QualRule, QualRuleset, Rule, Ruleset, SelectorRuleset};
34
+ pub use self::selector::{Combinator, Selector, SelectorAttr, SelectorPath, SelectorTerm};
35
+ pub use self::tree_ruleset::{TreeRule, TreeRuleset};
36
+ use crate::parser::*;
37
+ use crate::render::*;
38
+ use crate::transform::*;
39
+ use crate::transformers;
40
+
41
+ /// A non-nested "flat" CSS representation, suitable for browser output. The
42
+ /// [`Css`] AST is typically generated via the
43
+ /// [`crate::ast::Tree::flatten_tree`] method.
44
+ #[derive(Clone, Debug)]
45
+ pub struct Css<'a>(pub Vec<FlatRuleset<'a>>);
46
+
47
+ impl<'a> Css<'a> {
48
+ /// A mutable transform which walks this AST recursively, invoking `f` for
49
+ /// all nodes of type `T`.
50
+ ///
51
+ /// # Example
52
+ ///
53
+ /// ```
54
+ /// use procss::{ast, parse, RenderCss};
55
+ /// let mut css = parse("div{color: red;}").unwrap().flatten_tree();
56
+ /// css.transform(|rule: &mut ast::Rule| {
57
+ /// rule.value = "green".into();
58
+ /// });
59
+ ///
60
+ /// assert_eq!(css.as_css_string(), "div{color:green;}");
61
+ /// ```
62
+ pub fn transform<T>(&mut self, mut f: impl FnMut(&mut T))
63
+ where
64
+ Self: TransformCss<T>,
65
+ {
66
+ self.transform_each(&mut f)
67
+ }
68
+
69
+ /// Iterate over the immediate children of this Tree (non-recursive).
70
+ pub fn iter(&self) -> impl Iterator<Item = &'_ FlatRuleset<'a>> {
71
+ self.0.iter()
72
+ }
73
+ }
74
+
75
+ impl<'a, T> TransformCss<T> for Css<'a>
76
+ where
77
+ Ruleset<'a, Rule<'a>>: TransformCss<T>,
78
+ {
79
+ fn transform_each<F: FnMut(&mut T)>(&mut self, f: &mut F) {
80
+ for rule in self.0.iter_mut() {
81
+ rule.transform_each(f);
82
+ }
83
+ }
84
+ }
85
+
86
+ impl<'a> RenderCss for Css<'a> {
87
+ fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88
+ for x in self.0.iter() {
89
+ x.render(f)?;
90
+ }
91
+
92
+ Ok(())
93
+ }
94
+ }
95
+
96
+ /// A nested CSS representation yielded from parsing. [`Tree`] also implements
97
+ /// [`RenderCss`] if this is needed, though this output can't be read by
98
+ /// browsers and is not identical to the input since whitespace has been
99
+ /// discarded.
100
+ #[derive(Clone, Debug)]
101
+ pub struct Tree<'a>(pub Vec<TreeRuleset<'a>>);
102
+
103
+ impl<'a> Tree<'a> {
104
+ /// Flatten a nested [`Tree`] into a [`Css`], or in other words, convert the
105
+ /// result of a parse into something that can be rendered.
106
+ pub fn flatten_tree(&self) -> Css<'a> {
107
+ let mut rules = self
108
+ .0
109
+ .iter()
110
+ .flat_map(|x| x.flatten_tree())
111
+ .collect::<Vec<_>>();
112
+
113
+ rules.sort_by(|x, y| match (x, y) {
114
+ (Ruleset::QualRule(_), Ruleset::QualRule(_)) => Ordering::Equal,
115
+ (Ruleset::QualRule(_), _) => Ordering::Less,
116
+ (_, Ruleset::QualRule(_)) => Ordering::Greater,
117
+ _ => Ordering::Equal,
118
+ });
119
+
120
+ let mut css = Css(rules);
121
+ transformers::flat_self(&mut css);
122
+ css
123
+ }
124
+
125
+ /// A mutable transform which walks this AST recursively, invoking `f` for
126
+ /// all nodes of type `T`.
127
+ ///
128
+ /// # Example
129
+ ///
130
+ /// ```
131
+ /// use procss::{ast, parse, RenderCss};
132
+ /// let mut tree = parse("div{color: red;}").unwrap();
133
+ /// tree.transform(|rule: &mut ast::Rule| {
134
+ /// rule.value = "green".into();
135
+ /// });
136
+ ///
137
+ /// let css = tree.flatten_tree().as_css_string();
138
+ /// assert_eq!(css, "div{color:green;}");
139
+ /// ```
140
+ pub fn transform<T>(&mut self, mut f: impl FnMut(&mut T))
141
+ where
142
+ Self: TransformCss<T>,
143
+ {
144
+ self.transform_each(&mut f)
145
+ }
146
+
147
+ /// Iterate over the immediate children of this Tree (non-recursive).
148
+ pub fn iter(&self) -> impl Iterator<Item = &'_ TreeRuleset<'a>> {
149
+ self.0.iter()
150
+ }
151
+ }
152
+
153
+ impl<'a> TransformCss<Rule<'a>> for Tree<'a> {
154
+ fn transform_each<F: FnMut(&mut Rule<'a>)>(&mut self, f: &mut F) {
155
+ for rule in self.0.iter_mut() {
156
+ rule.transform_each(f);
157
+ }
158
+ }
159
+ }
160
+
161
+ impl<'a> TransformCss<TreeRuleset<'a>> for Tree<'a> {
162
+ fn transform_each<F: FnMut(&mut TreeRuleset<'a>)>(&mut self, f: &mut F) {
163
+ for rule in self.0.iter_mut() {
164
+ f(rule);
165
+ match rule {
166
+ Ruleset::SelectorRuleset(ruleset) => {
167
+ for rule in ruleset.1.iter_mut() {
168
+ rule.transform_each(f)
169
+ }
170
+ }
171
+ Ruleset::QualRule(_) => (),
172
+ Ruleset::QualRuleset(_) => (),
173
+ Ruleset::QualNestedRuleset(ruleset) => {
174
+ for rule in ruleset.1.iter_mut() {
175
+ rule.transform_each(f)
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ impl<'a> TransformCss<Vec<TreeRuleset<'a>>> for Tree<'a> {
184
+ fn transform_each<F: FnMut(&mut Vec<TreeRuleset<'a>>)>(&mut self, f: &mut F) {
185
+ f(&mut self.0);
186
+ for rule in self.0.iter_mut() {
187
+ // f(rule);
188
+ rule.transform_each(f);
189
+ }
190
+ }
191
+ }
192
+
193
+ impl<'a> ParseCss<'a> for Tree<'a> {
194
+ fn parse<E>(input: &'a str) -> IResult<&'a str, Self, E>
195
+ where
196
+ E: ParseError<&'a str>,
197
+ {
198
+ let (input, _) = token::sep0(input)?;
199
+ let (input, x) = alt((
200
+ eof.map(|_| vec![]),
201
+ terminated(many1(terminated(TreeRuleset::parse, token::sep0)), eof),
202
+ ))(input)?;
203
+
204
+ Ok((input, Tree(x)))
205
+ }
206
+ }
207
+
208
+ impl<'a> RenderCss for Tree<'a> {
209
+ fn render(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210
+ for x in self.0.iter() {
211
+ x.render(f)?;
212
+ }
213
+
214
+ Ok(())
215
+ }
216
+ }
package/src/builder.rs ADDED
@@ -0,0 +1,189 @@
1
+ // ┌───────────────────────────────────────────────────────────────────────────┐
2
+ // │ │
3
+ // │ ██████╗ ██████╗ ██████╗ Copyright (C) 2022, The Prospective Company │
4
+ // │ ██╔══██╗██╔══██╗██╔═══██╗ │
5
+ // │ ██████╔╝██████╔╝██║ ██║ This file is part of the Procss library, │
6
+ // │ ██╔═══╝ ██╔══██╗██║ ██║ distributed under the terms of the │
7
+ // │ ██║ ██║ ██║╚██████╔╝ Apache License 2.0. The full license can │
8
+ // │ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ be found in the LICENSE file. │
9
+ // │ │
10
+ // └───────────────────────────────────────────────────────────────────────────┘
11
+
12
+ use std::collections::HashMap;
13
+ use std::path::PathBuf;
14
+
15
+ use anyhow::Context;
16
+
17
+ use crate::parser::{unwrap_parse_error, ParseCss};
18
+ use crate::render::RenderCss;
19
+ #[cfg(not(target_arch = "wasm32"))]
20
+ use crate::utils::fs;
21
+ #[cfg(feature = "iotest")]
22
+ use crate::utils::IoTestFs;
23
+ use crate::{ast, transformers, utils};
24
+
25
+ /// A CSS+ project build, comprising a collection of CSS+ files which may
26
+ /// reference eachother (via `@import`).
27
+ pub struct BuildCss<'a> {
28
+ paths: Vec<String>,
29
+ contents: HashMap<&'a str, String>,
30
+ trees: HashMap<&'a str, ast::Tree<'a>>,
31
+ css: HashMap<&'a str, ast::Css<'a>>,
32
+ rootdir: String,
33
+ }
34
+
35
+ /// The compiled output of a [`BuildCss`] collection, obtained from
36
+ /// [`BuildCss::compile`].
37
+ pub struct CompiledCss<'a>(&'a BuildCss<'a>);
38
+
39
+ /// An incremental build struct for compiling a project's CSS sources.
40
+ ///
41
+ /// # Example
42
+ ///
43
+ /// ```no_run
44
+ /// let mut build = procss::BuildCss::new("./src");
45
+ /// build.add_file("app.scss");
46
+ /// build.compile().unwrap().write("./dist").unwrap();
47
+ /// ```
48
+ impl<'a> BuildCss<'a> {
49
+ /// Create a new [`BuildCss`] rooted at `rootdir`.
50
+ pub fn new<S: Into<String>>(rootdir: S) -> Self {
51
+ Self {
52
+ paths: Default::default(),
53
+ contents: Default::default(),
54
+ trees: Default::default(),
55
+ css: Default::default(),
56
+ rootdir: rootdir.into(),
57
+ }
58
+ }
59
+
60
+ /// Add a file `path` to this build.
61
+ #[cfg(not(target_arch = "wasm32"))]
62
+ pub fn add_file(&mut self, path: &'a str) {
63
+ self.paths.push(path.to_owned());
64
+ let inpath = PathBuf::from(&self.rootdir).join(path);
65
+ let txt = fs::read_to_string(inpath.as_path()).unwrap();
66
+ self.contents.insert(path, txt);
67
+ }
68
+
69
+ /// Add a file `path` to this build.
70
+ pub fn add_content(&mut self, path: &'a str, scss: String) {
71
+ self.paths.push(path.to_owned());
72
+ self.contents.insert(path, scss);
73
+ }
74
+
75
+ /// Compile this [`BuildCss`] start-to-finish, applying all transforms along
76
+ /// the way.
77
+ pub fn compile(&'a mut self) -> anyhow::Result<CompiledCss<'a>> {
78
+ for (path, contents) in &self.contents {
79
+ let tree = ast::Tree::parse(contents);
80
+ let (_, tree) = tree.map_err(|err| unwrap_parse_error(contents, err))?;
81
+ self.trees.insert(path, tree);
82
+ }
83
+
84
+ let dep_trees = self.trees.clone();
85
+ for (path, tree) in self.trees.iter_mut() {
86
+ transformers::apply_import(&dep_trees)(tree);
87
+ transformers::apply_mixin(tree);
88
+ transformers::apply_var(tree);
89
+ self.css.insert(path, tree.flatten_tree());
90
+ }
91
+
92
+ for (path, css) in self.css.iter_mut() {
93
+ let srcdir = utils::join_paths(&self.rootdir, path);
94
+ transformers::inline_url(&srcdir.to_string_lossy())(css);
95
+ transformers::dedupe(css);
96
+ }
97
+
98
+ Ok(CompiledCss(self))
99
+ }
100
+ }
101
+
102
+ impl<'a> CompiledCss<'a> {
103
+ /// Write this struct's compiled data to `outdir`, preserving the relative
104
+ /// subdirectory structure of the `input` sources passed to
105
+ /// [`BuildCss::add`], relative to `outdir`.
106
+ #[cfg(not(target_arch = "wasm32"))]
107
+ pub fn write(self, outdir: &'static str) -> anyhow::Result<()> {
108
+ for (outfile, css, path) in self.iter_files().flatten() {
109
+ let outdir = utils::join_paths(outdir, path);
110
+ fs::create_dir_all(outdir.clone()).unwrap_or_default();
111
+ fs::write(outdir.join(outfile), css)?;
112
+ }
113
+
114
+ Ok(())
115
+ }
116
+
117
+ /// Render this struct's compiled data in memory as a `String`, preserving
118
+ /// the relative subdirectory structure of the `input` sources passed to
119
+ /// [`BuildCss::add`], relative to `outdir`.
120
+ pub fn as_strings(&self) -> anyhow::Result<HashMap<String, String>> {
121
+ let mut results = HashMap::default();
122
+ for (outfile, css, _) in self.iter_files().flatten() {
123
+ results.insert(outfile, css).unwrap_or_default();
124
+ }
125
+
126
+ Ok(results)
127
+ }
128
+
129
+ fn iter_files(&self) -> impl Iterator<Item = anyhow::Result<(String, String, &'_ str)>> {
130
+ self.0.css.iter().map(|(path, css)| {
131
+ let outpath = PathBuf::from(path);
132
+ let outfile = format!(
133
+ "{}.css",
134
+ outpath
135
+ .file_prefix()
136
+ .context("No Prefix")?
137
+ .to_string_lossy()
138
+ );
139
+
140
+ Ok((outfile, css.as_css_string(), *path))
141
+ })
142
+ }
143
+ }
144
+
145
+ #[cfg(all(test, feature = "iotest"))]
146
+ mod tests {
147
+ use std::cell::RefCell;
148
+ use std::path::*;
149
+ use std::rc::Rc;
150
+
151
+ use super::*;
152
+
153
+ #[test]
154
+ fn test_simple_build() {
155
+ let outputs = Rc::new(RefCell::new(vec![]));
156
+ let infiles = Rc::new(RefCell::new(vec![]));
157
+ let outfiles = Rc::new(RefCell::new(vec![]));
158
+
159
+ let ctx = fs::read_to_string_context();
160
+ let infiles2 = infiles.clone();
161
+ ctx.expect().times(1).returning_st(move |x: &Path| {
162
+ infiles2.borrow_mut().push(x.to_string_lossy().to_string());
163
+ Ok("div{.open{color:green}}".to_owned())
164
+ });
165
+
166
+ let ctx = fs::create_dir_all_context();
167
+ ctx.expect().times(1).returning(|_: PathBuf| Ok(()));
168
+
169
+ let ctx = fs::write_context();
170
+ let outputs2 = outputs.clone();
171
+ let outfiles2 = outfiles.clone();
172
+ ctx.expect().returning_st(move |x: PathBuf, y: String| {
173
+ outfiles2.borrow_mut().push(x.to_string_lossy().to_string());
174
+ outputs2.borrow_mut().push(y);
175
+ Ok(())
176
+ });
177
+
178
+ let mut build = BuildCss::new("./src".to_owned());
179
+ build.add_file("app/component.scss");
180
+ build.compile().unwrap().write("./dist").unwrap();
181
+
182
+ let outputs = outputs.borrow().clone();
183
+ assert_eq!(outputs, vec!["div .open{color:green;}".to_owned()]);
184
+ let infiles = infiles.borrow().clone();
185
+ assert_eq!(infiles, vec!["./src/app/component.scss".to_owned()]);
186
+ let outfiles = outfiles.borrow().clone();
187
+ assert_eq!(outfiles, vec!["./dist/app/component.css".to_owned()])
188
+ }
189
+ }