rsass/output/
transform.rs

1//! This module provides `handle_body` (and internally `handle_item`),
2//! that does most of the work for [`crate::input::Context::transform`].
3
4use super::cssdest::CssDestination;
5use super::CssData;
6use crate::css::{self, AtRule, Import, SelectorCtx, Value};
7use crate::error::ResultPos;
8use crate::input::{Context, Loader, Parsed, SourceKind};
9use crate::sass::{get_global_module, Expose, Item, UseAs};
10use crate::{Error, Invalid, ScopeRef};
11
12pub fn handle_parsed(
13    items: Parsed,
14    dest: &mut dyn CssDestination,
15    scope: ScopeRef,
16    file_context: &mut Context<impl Loader>,
17) -> Result<(), Error> {
18    match items {
19        Parsed::Scss(items) => handle_body(&items, dest, scope, file_context),
20        Parsed::Css(items) => push_items(dest, items).no_pos(),
21    }
22}
23
24fn handle_body(
25    items: &[Item],
26    dest: &mut dyn CssDestination,
27    scope: ScopeRef,
28    file_context: &mut Context<impl Loader>,
29) -> Result<(), Error> {
30    for b in items {
31        handle_item(b, dest, scope.clone(), file_context)?;
32    }
33    Ok(())
34}
35
36fn handle_item(
37    item: &Item,
38    dest: &mut dyn CssDestination,
39    scope: ScopeRef,
40    file_context: &mut Context<impl Loader>,
41) -> Result<(), Error> {
42    match item {
43        Item::Use(ref name, ref as_n, ref with, ref pos) => {
44            let name = name.evaluate(scope.clone())?.take_value();
45            let module = if let Some(module) = get_global_module(&name) {
46                if !with.is_empty() {
47                    return Err(Invalid::ConfigBuiltin).at(pos);
48                }
49                module
50            } else if let Some(sourcefile) =
51                file_context.find_file(&name, SourceKind::Use(pos.clone()))?
52            {
53                let module = dest.head().load_module(
54                    sourcefile.path(),
55                    |dest| {
56                        let module = ScopeRef::new_global(scope.get_format());
57                        for (name, value, default) in with {
58                            let default = if *default {
59                                scope.get_or_none(name)
60                            } else {
61                                None
62                            };
63                            let value = default.ok_or(()).or_else(|()| {
64                                value.do_evaluate(scope.clone(), true)
65                            })?;
66                            if module.get_or_none(name).is_none() {
67                                module.define(name.clone(), value)?;
68                            } else {
69                                return Err(Error::S(
70                                    "The same variable may only be configured once.".to_string(),
71                                ));
72                            }
73                        }
74                        handle_parsed(
75                            sourcefile.parse()?,
76                            dest,
77                            module.clone(),
78                            file_context,
79                        )?;
80                        Ok(module)
81                    })?;
82                file_context.unlock_loading(&sourcefile);
83                module
84            } else {
85                return Err(Error::BadCall(
86                    "Can't find stylesheet to import.".into(),
87                    pos.clone(),
88                    None,
89                ));
90            };
91            scope.do_use(module, &name, as_n, &Expose::All)?;
92        }
93        Item::Forward(ref name, ref as_n, ref expose, ref with, ref pos) => {
94            let name = name.evaluate(scope.clone())?.take_value();
95            let module = if let Some(module) = get_global_module(&name) {
96                if !with.is_empty() {
97                    return Err(Invalid::ConfigBuiltin).at(pos);
98                }
99                module
100            } else if let Some(sourcefile) = file_context
101                .find_file(&name, SourceKind::Forward(pos.clone()))?
102            {
103                let module = dest.head().load_module(
104                    sourcefile.path(),
105                    |dest| {
106                        let module = ScopeRef::new_global(scope.get_format());
107                        for (name, value, default) in with {
108                            let default = if *default {
109                                scope.get_or_none(name)
110                            } else {
111                                None
112                            };
113                            let value = default.ok_or(()).or_else(|()| {
114                                value.do_evaluate(scope.clone(), true)
115                            })?;
116                            if module.get_or_none(name).is_none() {
117                                module.define(name.clone(), value)?;
118                            } else {
119                                return Err(Error::S(
120                                    "The same variable may only be configured once.".to_string(),
121                                ));
122                            }
123                        }
124                        handle_parsed(
125                            sourcefile.parse()?,
126                            dest,
127                            module.clone(),
128                            file_context,
129                        )?;
130                        Ok(module)
131                    });
132                file_context.unlock_loading(&sourcefile);
133                module?
134            } else {
135                return Err(Error::S(format!("Module {name} not found")));
136            };
137            scope.forward().do_use(module, &name, as_n, expose)?;
138        }
139        Item::Import(ref names, ref args, ref pos) => {
140            'name: for name in names {
141                let name = name.evaluate(scope.clone())?;
142                if args.is_null() {
143                    let x = name.value();
144                    if let Some(sourcefile) = file_context
145                        .find_file(x, SourceKind::Import(pos.clone()))?
146                    {
147                        match sourcefile.parse()? {
148                            Parsed::Scss(items) => {
149                                let mut thead = CssData::new();
150                                let module = ScopeRef::sub(scope.clone());
151                                let selectors = scope.get_selectors();
152                                if !selectors.is_root() {
153                                    let mut rule = thead
154                                        .start_rule(selectors.real())
155                                        .at(pos)?;
156                                    handle_body(
157                                        &items,
158                                        &mut rule,
159                                        module.clone(),
160                                        file_context,
161                                    )?;
162                                } else {
163                                    handle_body(
164                                        &items,
165                                        &mut thead,
166                                        module.clone(),
167                                        file_context,
168                                    )?;
169                                }
170                                push_items(dest, thead.into_iter())
171                                    .at(pos)?;
172                                scope.do_use(
173                                    module,
174                                    "",
175                                    &UseAs::Star,
176                                    &Expose::All,
177                                )?;
178                            }
179                            Parsed::Css(items) => {
180                                push_items(dest, items).at(pos)?;
181                            }
182                        }
183                        file_context.unlock_loading(&sourcefile);
184                        continue 'name;
185                    }
186                    if !(x.starts_with("http://")
187                        || x.starts_with("https://")
188                        || x.starts_with("//")
189                        || x.ends_with(".css")
190                        || name.is_css_url())
191                    {
192                        return Err(Error::BadCall(
193                            "Can't find stylesheet to import.".into(),
194                            pos.clone(),
195                            None,
196                        ));
197                    }
198                }
199                let args = args.evaluate(scope.clone())?;
200                dest.push_import(Import::new(Value::Literal(name), args));
201            }
202        }
203        Item::AtRoot(ref selectors, ref body) => {
204            let selectors = selectors.eval(scope.clone())?;
205            let ctx = scope.get_selectors().at_root(selectors);
206            let selectors = ctx.real();
207            let subscope = ScopeRef::sub_selectors(scope, ctx);
208            if !selectors.is_root() {
209                let mut rule = dest.start_rule(selectors).no_pos()?;
210                handle_body(body, &mut rule, subscope, file_context)?;
211            } else {
212                handle_body(body, dest, subscope, file_context)?;
213            }
214        }
215        Item::AtMedia { args, body, pos: _ } => {
216            let args = args.evaluate(scope.clone())?;
217            let mut atmedia = dest.start_atmedia(args.try_into()?);
218            if let Some(ref body) = *body {
219                let local = ScopeRef::sub(scope);
220                handle_body(body, &mut atmedia, local, file_context)?;
221            }
222        }
223        Item::AtRule {
224            name,
225            args,
226            body,
227            pos,
228        } => {
229            let name = name.evaluate(scope.clone())?.take_value();
230            let args = args.evaluate(scope.clone())?;
231            if let Some(ref body) = *body {
232                let mut atrule = dest.start_atrule(name.clone(), args);
233                let local = if name == "keyframes" {
234                    ScopeRef::sub_selectors(scope, SelectorCtx::root())
235                } else {
236                    ScopeRef::sub(scope)
237                };
238                handle_body(body, &mut atrule, local, file_context)?;
239            } else {
240                dest.push_item(AtRule::new(name, args, None).into())
241                    .at(pos)?;
242            }
243        }
244        Item::VariableDeclaration(ref var) => {
245            var.evaluate(&scope)?;
246        }
247        Item::FunctionDeclaration(ref name, ref body) => {
248            if name == "calc"
249                || name == "element"
250                || name == "expression"
251                || name == "url"
252            {
253                // Ok, this is cheating for the test suite ...
254                let p = body.decl.clone().opt_back("@function ");
255                return Err(Invalid::FunctionName.at(p));
256            }
257            check_body(&body.body, BodyContext::Function)?;
258            scope.define_function(name.into(), body.closure(&scope).into());
259        }
260        Item::Return(_, ref pos) => {
261            return Err(Invalid::AtRule.at(pos.clone()));
262        }
263
264        Item::MixinDeclaration(ref name, ref body) => {
265            check_body(&body.body, BodyContext::Mixin)?;
266            scope.define_mixin(name.into(), body.closure(&scope).into());
267        }
268        Item::MixinCall(ref name, ref args, ref body, ref pos) => {
269            if let Some(mixin) = scope.get_mixin(&name.into()) {
270                let mixin = mixin
271                    .get(scope.clone(), args, pos, file_context)
272                    .map_err(|e| e.called_from(pos, name))?;
273                mixin.define_content(&scope, body);
274                handle_parsed(mixin.body, dest, mixin.scope, file_context)
275                    .map_err(|e: Error| match e {
276                        Error::Invalid(err, _) => err.at(pos.clone()),
277                        Error::BadCall(msg, pos, p2) => {
278                            Error::BadCall(msg, pos.in_call(name), p2)
279                        }
280                        e => {
281                            let pos = pos.in_call(name);
282                            Error::BadCall(e.to_string(), pos, None)
283                        }
284                    })?;
285            } else {
286                return Err(Error::BadCall(
287                    "Undefined mixin.".into(),
288                    pos.clone(),
289                    None,
290                ));
291            }
292        }
293        Item::Content(args, pos) => {
294            if let Some(content) = scope.get_content() {
295                let mixin = content
296                    .get(scope, args, pos, file_context)
297                    .map_err(|e| e.called_from(pos, "@content"))?;
298                handle_parsed(mixin.body, dest, mixin.scope, file_context)?;
299            }
300        }
301
302        Item::IfStatement(ref cond, ref do_if, ref do_else) => {
303            let cond = cond.evaluate(scope.clone())?.is_true();
304            let items = if cond { do_if } else { do_else };
305            check_body(items, BodyContext::Control)?;
306            handle_body(items, dest, scope, file_context)?;
307        }
308        Item::Each(ref names, ref values, ref body) => {
309            check_body(body, BodyContext::Control)?;
310            let pushed = scope.store_local_values(names);
311            for value in values.evaluate(scope.clone())?.iter_items() {
312                scope.define_multi(names, value)?;
313                handle_body(body, dest, scope.clone(), file_context)?;
314            }
315            scope.restore_local_values(pushed);
316        }
317        Item::For(ref name, ref range, ref body) => {
318            let range = range.evaluate(scope.clone())?;
319            check_body(body, BodyContext::Control)?;
320            for value in range {
321                let scope = ScopeRef::sub(scope.clone());
322                scope.define(name.clone(), value)?;
323                handle_body(body, dest, scope, file_context)?;
324            }
325        }
326        Item::While(ref cond, ref body) => {
327            check_body(body, BodyContext::Control)?;
328            let scope = ScopeRef::sub(scope);
329            while cond.evaluate(scope.clone())?.is_true() {
330                handle_body(body, dest, scope.clone(), file_context)?;
331            }
332        }
333
334        Item::Debug(ref value) => {
335            eprintln!("DEBUG: {}", value.evaluate(scope)?.introspect());
336        }
337        Item::Warn(ref value) => {
338            eprintln!("WARNING: {}", value.evaluate(scope)?.introspect());
339        }
340        Item::Error(ref value, ref pos) => {
341            let msg = value.evaluate(scope)?.introspect();
342            return Err(Invalid::AtError(msg).at(pos.clone()));
343        }
344        Item::Extend(_selectors) => {
345            return Err(Error::S("@extend is not supported yet".to_string()));
346        }
347
348        Item::Rule(ref selectors, ref body) => {
349            check_body(body, BodyContext::Rule)?;
350            let selectors = selectors.eval(scope.clone())?;
351            let selectors = scope.get_selectors().nest(selectors);
352            let mut dest = dest.start_rule(selectors.clone()).no_pos()?;
353            let scope = ScopeRef::sub_selectors(scope, selectors.into());
354            handle_body(body, &mut dest, scope, file_context)?;
355        }
356        Item::Property(ref name, ref value, ref pos) => {
357            let v = value.evaluate(scope.clone())?;
358            if !v.is_null() {
359                let name = name.evaluate(scope)?.take_value();
360                // Note: inner pos here is correctly the value pos,
361                // but the outher should probably be the entire property.
362                dest.push_property(name, v.valid_css().at(pos)?).at(pos)?;
363            }
364        }
365        Item::CustomProperty(ref name, ref value) => {
366            let v = value.evaluate(scope.clone())?;
367            if !v.is_null() {
368                let name = name.evaluate(scope)?.take_value();
369                dest.push_custom_property(name, v).no_pos()?;
370            }
371        }
372        Item::NamespaceRule(ref name, ref value, ref body) => {
373            check_body(body, BodyContext::NsRule)?;
374            let value = value.evaluate(scope.clone())?;
375            let name = name.evaluate(scope.clone())?.take_value();
376            if !value.is_null() {
377                dest.push_property(name.clone(), value).no_pos()?;
378            }
379            let mut dest = dest.start_nsrule(name).no_pos()?;
380            handle_body(body, &mut dest, scope, file_context)?;
381        }
382        Item::Comment(ref c) => {
383            if !scope.get_format().is_compressed() {
384                dest.push_comment(c.evaluate(scope)?.take_value().into());
385            }
386        }
387        Item::None => (),
388    }
389    Ok(())
390}
391
392#[derive(Clone, Copy, PartialEq, Eq)]
393enum BodyContext {
394    Mixin,
395    Function,
396    Control,
397    Rule,
398    NsRule,
399}
400
401fn check_body(body: &[Item], context: BodyContext) -> Result<(), Error> {
402    for item in body {
403        match item {
404            Item::Forward(_, _, _, _, pos) => {
405                return Err(Invalid::AtRule.at(pos.clone()));
406            }
407            Item::Use(_, _, _, pos) => {
408                return Err(Invalid::AtRule.at(pos.clone()));
409            }
410            Item::MixinDeclaration(.., ref decl) => {
411                let pos = decl.decl.clone().opt_back("@mixin ");
412                match context {
413                    BodyContext::Mixin => {
414                        return Err(Invalid::MixinInMixin.at(pos));
415                    }
416                    BodyContext::Control => {
417                        return Err(Invalid::MixinInControl.at(pos));
418                    }
419                    BodyContext::Rule => (), // This is ok
420                    _ => {
421                        return Err(Invalid::AtRule.at(pos.opt_trail_ws()));
422                    }
423                }
424            }
425            Item::FunctionDeclaration(_, ref body) => {
426                let pos = body.decl.clone().opt_back("@function ");
427                match context {
428                    BodyContext::Mixin => {
429                        return Err(Invalid::FunctionInMixin.at(pos));
430                    }
431                    BodyContext::Control => {
432                        return Err(Invalid::FunctionInControl.at(pos));
433                    }
434                    BodyContext::Rule => (), // This is ok
435                    _ => {
436                        return Err(Invalid::AtRule.at(pos.opt_trail_ws()));
437                    }
438                }
439            }
440            Item::Return(_, ref pos) if context != BodyContext::Function => {
441                return Err(Invalid::AtRule.at(pos.clone()));
442            }
443            Item::AtRule {
444                name,
445                args: _,
446                body: _,
447                pos,
448            } if context != BodyContext::Rule => {
449                if !name
450                    .single_raw()
451                    .map_or(false, |name| name_in(name, &CSS_AT_RULES[..]))
452                {
453                    return Err(Invalid::AtRule.at(pos.clone()));
454                }
455            }
456            _ => (),
457        }
458    }
459    Ok(())
460}
461
462const CSS_AT_RULES: [&str; 16] = [
463    "charset",
464    "color-profile",
465    "counter-style",
466    "document",
467    "font-face",
468    "font-feature-values",
469    "import",
470    "keyframes",
471    "layer",
472    "media",
473    "namespace",
474    "page",
475    "property",
476    "scroll-timeline",
477    "supports",
478    "viewport",
479];
480
481fn push_items(
482    dest: &mut dyn CssDestination,
483    items: impl IntoIterator<Item = css::Item>,
484) -> Result<(), Invalid> {
485    for item in items {
486        dest.push_item(item)?;
487    }
488    Ok(())
489}
490
491fn name_in(name: &str, known: &[&str]) -> bool {
492    if name.starts_with('-') {
493        known.iter().any(|end| {
494            name.strip_suffix(end).map_or(false, |s| s.ends_with('-'))
495        })
496    } else {
497        known.iter().any(|known| name == *known)
498    }
499}