1use 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 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 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 => (), _ => {
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 => (), _ => {
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}