1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use super::{
    CallArgs, CallError, Callable, Closure, FormalArgs, Name, ResolvedArgs,
    Value,
};
use crate::css::{self, CssString, ValueToMapError};
use crate::input::{Context, Loader, Parsed, SourceKind, SourcePos};
use crate::ordermap::OrderMap;
use crate::ScopeRef;

/// A declared mixin
#[derive(Clone)]
pub enum MixinDecl {
    /// an actual mixin
    Sass(Closure),
    /// The body of a mixin call that does not have a body
    NoBody,
    /// The special `load-css` mixin.
    LoadCss,
}

impl From<Closure> for MixinDecl {
    fn from(decl: Closure) -> Self {
        Self::Sass(decl)
    }
}

impl MixinDecl {
    pub(crate) fn get(
        self,
        scope: ScopeRef,
        call_args: &CallArgs,
        call_pos: &SourcePos,
        file_context: &mut Context<impl Loader>,
    ) -> Result<Mixin, CallError> {
        match self {
            Self::Sass(decl) => {
                let sel = scope.get_selectors().clone();
                Ok(Mixin {
                    scope: decl.eval_args(
                        ScopeRef::sub_selectors(decl.scope.clone(), sel),
                        call_args.evaluate(scope)?.args,
                    )?,
                    body: Parsed::Scss(decl.body.body),
                })
            }
            Self::NoBody => Ok(Mixin::empty(scope)),
            Self::LoadCss => {
                let fargs = FormalArgs::new(vec![
                    (name!(url), None),
                    (name!(with), Some(Value::Null)),
                ]);
                let pos = SourcePos::mock_mixin(
                    &name!(load_css),
                    &fargs,
                    "sass:meta",
                );
                let argscope = fargs
                    .eval_call(
                        scope.clone(),
                        call_args.evaluate(scope.clone())?,
                    )
                    .map_err(|e| e.declared_at(&pos))?;
                let url: CssString = argscope.get(name!(url))?;
                let with = get_opt_map(&argscope, name!(with))?;

                if url.value().starts_with("sass:") {
                    return if with.unwrap_or_default().is_empty() {
                        Ok(Mixin::empty(scope))
                    } else {
                        Err(CallError::msg(format!(
                            "Built-in module {} can't be configured.",
                            url.value()
                        )))
                    };
                }
                let source = file_context
                    .find_file(url.value(), SourceKind::load_css(call_pos))?
                    .ok_or_else(|| {
                        CallError::msg("Can't find stylesheet to import.")
                    })?;

                let scope = ScopeRef::sub(scope);
                if let Some(with) = with {
                    for (key, value) in with {
                        scope.define(key.into(), value)?;
                    }
                }
                file_context.unlock_loading(&source);
                Ok(Mixin {
                    scope,
                    body: source.parse()?,
                })
            }
        }
    }
    pub(crate) fn is_no_body(&self) -> bool {
        matches!(self, Self::NoBody)
    }
}

/// A mixin is a callable body of items.
#[derive(Clone)]
pub struct Mixin {
    /// The scope where this mixin is defined.
    pub scope: ScopeRef,
    /// The body of this mixin.
    pub body: Parsed,
}

impl Mixin {
    fn empty(scope: ScopeRef) -> Self {
        Self {
            scope,
            body: Parsed::Css(vec![]),
        }
    }
    pub(crate) fn define_content(
        &self,
        scope: &ScopeRef,
        body: &Option<Callable>,
    ) {
        self.scope.define_content(match body {
            Some(body) => body.closure(scope).into(),
            None => MixinDecl::NoBody,
        });
    }
}

fn get_opt_map(
    s: &ResolvedArgs,
    name: Name,
) -> Result<Option<OrderMap<CssString, css::Value>>, CallError> {
    match s.get(name.clone())? {
        css::Value::Null => Ok(None),
        v => v.try_into().map(Some).map_err(|e| match e {
            ValueToMapError::Root(err) => CallError::BadArgument(name, err),
            ValueToMapError::Key(err) => {
                CallError::msg(format!("${name} key: {err}"))
            }
        }),
    }
}