grass/src/stylesheet.rs

466 lines
18 KiB
Rust
Raw Normal View History

2020-04-27 15:53:43 -04:00
use std::fs;
use std::iter::Iterator;
use std::path::Path;
use codemap::{CodeMap, Span, Spanned};
use peekmore::{PeekMore, PeekMoreIterator};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
use crate::atrule::{eat_include, AtRule, AtRuleKind};
use crate::common::Identifier;
2020-04-27 15:53:43 -04:00
use crate::error::{SassError, SassResult};
use crate::imports::import;
use crate::lexer::Lexer;
use crate::output::Css;
use crate::scope::{
global_var_exists, insert_global_fn, insert_global_mixin, insert_global_var, Scope,
GLOBAL_SCOPE,
};
use crate::selector::Selector;
use crate::token::Token;
use crate::utils::{
devour_whitespace, eat_comment, eat_ident, eat_variable_value, parse_quoted_string,
2020-05-21 13:25:37 -04:00
peek_ident_no_interpolation, peek_whitespace, read_until_newline, VariableDecl,
2020-04-27 15:53:43 -04:00
};
use crate::{eat_expr, Expr, RuleSet, Stmt};
/// Represents a parsed SASS stylesheet with nesting
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct StyleSheet(pub(crate) Vec<Spanned<Stmt>>);
#[cfg(feature = "wasm")]
#[wasm_bindgen]
impl StyleSheet {
pub fn new(input: String) -> Result<String, JsValue> {
let mut map = CodeMap::new();
let file = map.add_file("stdin".into(), input);
Ok(Css::from_stylesheet(StyleSheet(
StyleSheetParser {
lexer: Lexer::new(&file).peekmore(),
nesting: 0,
2020-05-05 19:12:17 -04:00
map: &mut map,
2020-04-27 15:53:43 -04:00
path: Path::new(""),
}
.parse_toplevel()
.map_err(|e| raw_to_parse_error(&map, e).to_string())?
.0,
))
.map_err(|e| raw_to_parse_error(&map, e).to_string())?
2020-05-05 19:12:17 -04:00
.pretty_print(&map)
2020-04-27 15:53:43 -04:00
.map_err(|e| raw_to_parse_error(&map, e).to_string())?)
}
}
fn raw_to_parse_error(map: &CodeMap, err: SassError) -> SassError {
let (message, span) = err.raw();
SassError::from_loc(message, map.look_up_span(span))
}
impl StyleSheet {
/// Write CSS to `buf`, constructed from a string
///
/// ```
/// use grass::{SassResult, StyleSheet};
///
/// fn main() -> SassResult<()> {
/// let sass = StyleSheet::new("a { b { color: red; } }".to_string())?;
/// assert_eq!(sass, "a b {\n color: red;\n}\n");
/// Ok(())
/// }
/// ```
2020-05-01 15:43:43 -04:00
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
2020-04-27 15:53:43 -04:00
#[cfg(not(feature = "wasm"))]
pub fn new(input: String) -> SassResult<String> {
let mut map = CodeMap::new();
let file = map.add_file("stdin".into(), input);
Css::from_stylesheet(StyleSheet(
StyleSheetParser {
2020-05-21 13:25:37 -04:00
lexer: &mut Lexer::new(&file).peekmore(),
2020-04-27 15:53:43 -04:00
nesting: 0,
map: &mut map,
path: Path::new(""),
}
.parse_toplevel()
.map_err(|e| raw_to_parse_error(&map, e))?
.0,
))
.map_err(|e| raw_to_parse_error(&map, e))?
.pretty_print(&map)
.map_err(|e| raw_to_parse_error(&map, e))
}
/// Write CSS to `buf`, constructed from a path
///
/// ```
/// use grass::{SassResult, StyleSheet};
///
/// fn main() -> SassResult<()> {
/// let sass = StyleSheet::from_path("input.scss")?;
/// Ok(())
/// }
/// ```
2020-05-01 15:43:43 -04:00
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
2020-04-27 15:53:43 -04:00
#[cfg(not(feature = "wasm"))]
pub fn from_path(p: &str) -> SassResult<String> {
let mut map = CodeMap::new();
2020-04-28 08:27:35 -04:00
let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?);
2020-04-27 15:53:43 -04:00
Css::from_stylesheet(StyleSheet(
StyleSheetParser {
2020-05-21 13:25:37 -04:00
lexer: &mut Lexer::new(&file).peekmore(),
2020-04-27 15:53:43 -04:00
nesting: 0,
map: &mut map,
path: p.as_ref(),
}
.parse_toplevel()
.map_err(|e| raw_to_parse_error(&map, e))?
.0,
))
.map_err(|e| raw_to_parse_error(&map, e))?
.pretty_print(&map)
.map_err(|e| raw_to_parse_error(&map, e))
}
pub(crate) fn export_from_path<P: AsRef<Path> + Into<String> + Clone>(
p: &P,
map: &mut CodeMap,
) -> SassResult<(Vec<Spanned<Stmt>>, Scope)> {
let file = map.add_file(p.clone().into(), String::from_utf8(fs::read(p)?)?);
Ok(StyleSheetParser {
2020-05-21 13:25:37 -04:00
lexer: &mut Lexer::new(&file).peekmore(),
2020-04-27 15:53:43 -04:00
nesting: 0,
map,
path: p.as_ref(),
}
.parse_toplevel()?)
}
pub(crate) fn from_stmts(s: Vec<Spanned<Stmt>>) -> StyleSheet {
StyleSheet(s)
}
}
struct StyleSheetParser<'a> {
2020-05-21 13:25:37 -04:00
lexer: &'a mut PeekMoreIterator<Lexer<'a>>,
2020-04-27 15:53:43 -04:00
nesting: u32,
map: &'a mut CodeMap,
path: &'a Path,
}
fn is_selector_char(c: char) -> bool {
c.is_alphanumeric()
|| matches!(
c,
'_' | '-' | '[' | '#' | ':' | '*' | '%' | '.' | '>' | '\\'
)
}
2020-04-27 15:53:43 -04:00
impl<'a> StyleSheetParser<'a> {
fn parse_toplevel(mut self) -> SassResult<(Vec<Spanned<Stmt>>, Scope)> {
let mut rules: Vec<Spanned<Stmt>> = Vec::new();
while let Some(Token { kind, .. }) = self.lexer.peek() {
match kind {
_ if is_selector_char(*kind) => {
rules.extend(self.eat_rules(&Selector::new(), &mut Scope::new())?)
}
2020-05-20 21:01:07 -04:00
'\t' | '\n' | ' ' => {
2020-04-27 15:53:43 -04:00
self.lexer.next();
continue;
}
'$' => {
2020-05-21 13:25:37 -04:00
self.lexer.next();
let name = peek_ident_no_interpolation(self.lexer, false)?;
let whitespace = peek_whitespace(self.lexer);
match self.lexer.peek() {
Some(Token { kind: ':', .. }) => {
self.lexer
.take(name.node.chars().count() + whitespace + 1)
2020-05-21 13:25:37 -04:00
.for_each(drop);
devour_whitespace(self.lexer);
let name = name.map_node(|n| Identifier::from(n));
2020-05-21 13:25:37 -04:00
let VariableDecl { val, default, .. } =
eat_variable_value(self.lexer, &Scope::new(), &Selector::new())?;
2020-05-21 13:25:37 -04:00
if !(default && global_var_exists(name.node)) {
insert_global_var(name.node, val)?;
2020-05-21 13:25:37 -04:00
}
}
Some(..) | None => return Err(("expected \":\".", name.span).into()),
2020-04-27 15:53:43 -04:00
}
}
'/' => {
2020-05-20 21:01:07 -04:00
let pos = self.lexer.next().unwrap().pos;
match self.lexer.next() {
Some(Token { kind: '/', .. }) => {
2020-05-21 13:25:37 -04:00
read_until_newline(self.lexer);
devour_whitespace(self.lexer);
2020-05-20 21:01:07 -04:00
}
Some(Token { kind: '*', .. }) => {
2020-05-21 13:25:37 -04:00
let comment = eat_comment(self.lexer, &Scope::new(), &Selector::new())?;
2020-05-20 21:01:07 -04:00
rules.push(comment.map_node(Stmt::MultilineComment));
}
_ => return Err(("expected selector.", pos).into()),
2020-04-27 15:53:43 -04:00
}
}
'@' => {
let span_before = self.lexer.next().unwrap().pos();
let Spanned {
node: at_rule_kind,
span,
} = eat_ident(self.lexer, &Scope::new(), &Selector::new(), span_before)?;
2020-04-27 15:53:43 -04:00
if at_rule_kind.is_empty() {
return Err(("Expected identifier.", span).into());
}
match AtRuleKind::from(at_rule_kind.as_str()) {
AtRuleKind::Include => rules.extend(eat_include(
2020-05-21 13:25:37 -04:00
self.lexer,
2020-04-27 15:53:43 -04:00
&Scope::new(),
&Selector::new(),
None,
span,
2020-04-27 15:53:43 -04:00
)?),
AtRuleKind::Import => {
2020-05-21 13:25:37 -04:00
devour_whitespace(self.lexer);
2020-04-27 15:53:43 -04:00
let mut file_name = String::new();
let next = match self.lexer.next() {
Some(v) => v,
None => todo!("expected input after @import"),
2020-04-27 15:53:43 -04:00
};
match next.kind {
q @ '"' | q @ '\'' => {
file_name.push_str(
&parse_quoted_string(
2020-05-21 13:25:37 -04:00
self.lexer,
2020-04-27 15:53:43 -04:00
&Scope::new(),
q,
&Selector::new(),
)?
.node
.unquote()
.to_css_string(span)?,
);
2020-04-27 15:53:43 -04:00
}
_ => return Err(("Expected string.", next.pos()).into()),
}
if let Some(t) = self.lexer.peek() {
if t.kind == ';' {
self.lexer.next();
}
}
2020-05-21 13:25:37 -04:00
devour_whitespace(self.lexer);
2020-04-27 15:53:43 -04:00
let (new_rules, new_scope) =
import(self.path, file_name.as_ref(), &mut self.map)?;
2020-04-27 15:53:43 -04:00
rules.extend(new_rules);
GLOBAL_SCOPE.with(|s| {
s.borrow_mut().extend(new_scope);
});
}
v => {
let rule = AtRule::from_tokens(
v,
span,
self.lexer,
&mut Scope::new(),
&Selector::new(),
None,
)?;
2020-04-27 15:53:43 -04:00
match rule.node {
AtRule::Mixin(name, mixin) => {
insert_global_mixin(&name, *mixin);
}
AtRule::Function(name, func) => {
insert_global_fn(&name, *func);
}
AtRule::Charset => continue,
AtRule::Warn(message) => self.warn(rule.span, &message),
AtRule::Debug(message) => self.debug(rule.span, &message),
AtRule::Return(_) => {
return Err(
("This at-rule is not allowed here.", rule.span).into()
)
}
AtRule::For(f) => rules.extend(f.ruleset_eval(
&mut Scope::new(),
&Selector::new(),
None,
)?),
AtRule::While(w) => rules.extend(w.ruleset_eval(
&mut Scope::new(),
&Selector::new(),
true,
None,
)?),
AtRule::Each(e) => rules.extend(e.ruleset_eval(
&mut Scope::new(),
&Selector::new(),
None,
)?),
2020-04-27 15:53:43 -04:00
AtRule::Include(s) => rules.extend(s),
AtRule::Content => {
return Err((
"@content is only allowed within mixin declarations.",
rule.span,
)
.into())
}
2020-04-27 15:53:43 -04:00
AtRule::If(i) => {
rules.extend(i.eval(
&mut Scope::new(),
&Selector::new(),
None,
)?);
2020-04-27 15:53:43 -04:00
}
AtRule::AtRoot(root_rules) => rules.extend(root_rules),
AtRule::Unknown(..) => rules.push(rule.map_node(Stmt::AtRule)),
2020-05-20 20:13:53 -04:00
AtRule::Media(..) => rules.push(rule.map_node(Stmt::AtRule)),
2020-04-27 15:53:43 -04:00
}
}
}
}
2020-04-27 15:53:43 -04:00
'&' => {
return Err((
"Top-level selectors may not contain the parent selector \"&\".",
self.lexer.next().unwrap().pos(),
2020-04-27 15:53:43 -04:00
)
.into())
2020-04-27 15:53:43 -04:00
}
c if c.is_control() => {
return Err(("expected selector.", self.lexer.next().unwrap().pos()).into());
}
2020-05-21 12:06:42 -04:00
',' | '!' | '{' => {
2020-05-21 00:46:06 -04:00
return Err(("expected \"{\".", self.lexer.next().unwrap().pos()).into());
}
'`' | '\'' | '"' => {
2020-05-21 01:04:11 -04:00
return Err(("expected selector.", self.lexer.next().unwrap().pos()).into());
}
2020-04-27 15:53:43 -04:00
_ => todo!("unexpected toplevel token: {:?}", kind),
};
}
Ok((rules, GLOBAL_SCOPE.with(|s| s.borrow().clone())))
}
fn eat_rules(
&mut self,
super_selector: &Selector,
scope: &mut Scope,
) -> SassResult<Vec<Spanned<Stmt>>> {
let mut stmts = Vec::new();
2020-05-21 13:25:37 -04:00
while let Some(expr) = eat_expr(self.lexer, scope, super_selector, None)? {
2020-04-27 15:53:43 -04:00
let span = expr.span;
match expr.node {
Expr::Style(s) => stmts.push(Spanned {
node: Stmt::Style(s),
span,
}),
Expr::AtRule(a) => match a {
AtRule::For(f) => stmts.extend(f.ruleset_eval(scope, super_selector, None)?),
AtRule::While(w) => {
stmts.extend(w.ruleset_eval(scope, super_selector, false, None)?)
}
AtRule::Each(e) => stmts.extend(e.ruleset_eval(scope, super_selector, None)?),
AtRule::Include(s) => stmts.extend(s),
AtRule::If(i) => stmts.extend(i.eval(scope, super_selector, None)?),
AtRule::Content => {
return Err((
"@content is only allowed within mixin declarations.",
expr.span,
)
.into())
}
AtRule::Return(..) => {
return Err(("This at-rule is not allowed here.", expr.span).into())
}
AtRule::AtRoot(root_stmts) => stmts.extend(root_stmts),
AtRule::Debug(ref message) => self.debug(expr.span, message),
AtRule::Warn(ref message) => self.warn(expr.span, message),
AtRule::Mixin(..) | AtRule::Function(..) => todo!(),
AtRule::Charset => todo!(),
r @ AtRule::Unknown(..) => stmts.push(Spanned {
node: Stmt::AtRule(r),
span,
}),
2020-05-20 20:13:53 -04:00
r @ AtRule::Media(..) => stmts.push(Spanned {
node: Stmt::AtRule(r),
span,
}),
2020-04-27 15:53:43 -04:00
},
Expr::Styles(s) => stmts.extend(
s.into_iter()
.map(Box::new)
.map(Stmt::Style)
.map(|style| Spanned { node: style, span }),
),
Expr::MixinDecl(name, mixin) => {
scope.insert_mixin(&name, *mixin);
}
Expr::FunctionDecl(name, func) => {
scope.insert_fn(&name, *func);
}
Expr::Selector(s) => {
self.nesting += 1;
let rules = self.eat_rules(&super_selector.zip(&s), scope)?;
stmts.push(Spanned {
node: Stmt::RuleSet(RuleSet {
super_selector: super_selector.clone(),
selector: s,
rules,
}),
span,
});
self.nesting -= 1;
if self.nesting == 0 {
return Ok(stmts);
}
}
Expr::VariableDecl(name, val) => {
if self.nesting == 0 {
scope.insert_var(&name, *val.clone())?;
insert_global_var(&name, *val)?;
} else {
scope.insert_var(&name, *val)?;
}
}
Expr::MultilineComment(s) => stmts.push(Spanned {
node: Stmt::MultilineComment(s),
span,
}),
}
}
Ok(stmts)
}
}
/// Functions that print to stdout or stderr
impl<'a> StyleSheetParser<'a> {
fn debug(&self, span: Span, message: &str) {
let loc = self.map.look_up_span(span);
eprintln!(
"{}:{} Debug: {}",
loc.file.name(),
loc.begin.line + 1,
message
);
}
fn warn(&self, span: Span, message: &str) {
let loc = self.map.look_up_span(span);
eprintln!(
"Warning: {}\n {} {}:{} root stylesheet",
message,
loc.file.name(),
loc.begin.line + 1,
loc.begin.column + 1
);
}
}