refactor stylesheet into separate file
This commit is contained in:
parent
769b7628d8
commit
8711e61398
393
src/lib.rs
393
src/lib.rs
@ -80,33 +80,26 @@ grass input.scss
|
|||||||
clippy::redundant_pub_crate,
|
clippy::redundant_pub_crate,
|
||||||
)]
|
)]
|
||||||
#![cfg_attr(feature = "nightly", feature(track_caller))]
|
#![cfg_attr(feature = "nightly", feature(track_caller))]
|
||||||
use std::fs;
|
|
||||||
use std::iter::Iterator;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use codemap::{CodeMap, Span, Spanned};
|
use std::iter::Iterator;
|
||||||
|
|
||||||
|
use codemap::{Span, Spanned};
|
||||||
|
|
||||||
use peekmore::{PeekMore, PeekMoreIterator};
|
use peekmore::{PeekMore, PeekMoreIterator};
|
||||||
|
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use crate::atrule::{eat_include, AtRule, AtRuleKind, Function, Mixin};
|
use crate::atrule::{AtRule, AtRuleKind, Function, Mixin};
|
||||||
pub use crate::error::{SassError, SassResult};
|
pub use crate::error::{SassError, SassResult};
|
||||||
use crate::imports::import;
|
use crate::scope::{insert_global_var, Scope};
|
||||||
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::selector::Selector;
|
||||||
use crate::style::Style;
|
use crate::style::Style;
|
||||||
|
pub use crate::stylesheet::StyleSheet;
|
||||||
pub(crate) use crate::token::Token;
|
pub(crate) use crate::token::Token;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
devour_whitespace, eat_comment, eat_ident, eat_ident_no_interpolation, eat_variable_value,
|
devour_whitespace, eat_comment, eat_ident, eat_ident_no_interpolation, eat_variable_value,
|
||||||
parse_quoted_string, read_until_closing_curly_brace, read_until_closing_paren,
|
read_until_closing_curly_brace, read_until_closing_paren, read_until_newline, VariableDecl,
|
||||||
read_until_newline, VariableDecl,
|
|
||||||
};
|
};
|
||||||
use crate::value::Value;
|
use crate::value::Value;
|
||||||
|
|
||||||
@ -122,16 +115,12 @@ mod output;
|
|||||||
mod scope;
|
mod scope;
|
||||||
mod selector;
|
mod selector;
|
||||||
mod style;
|
mod style;
|
||||||
|
mod stylesheet;
|
||||||
mod token;
|
mod token;
|
||||||
mod unit;
|
mod unit;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod value;
|
mod value;
|
||||||
|
|
||||||
/// Represents a parsed SASS stylesheet with nesting
|
|
||||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct StyleSheet(Vec<Spanned<Stmt>>);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum Stmt {
|
pub(crate) enum Stmt {
|
||||||
/// A [`Style`](/grass/style/struct.Style)
|
/// A [`Style`](/grass/style/struct.Style)
|
||||||
@ -188,348 +177,6 @@ enum Expr {
|
|||||||
AtRule(AtRule),
|
AtRule(AtRule),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn raw_to_parse_error(map: &CodeMap, err: SassError) -> SassError {
|
|
||||||
let (message, span) = err.raw();
|
|
||||||
SassError::from_loc(message, map.look_up_span(span))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
map: &map,
|
|
||||||
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())?
|
|
||||||
.pretty_print()
|
|
||||||
.map_err(|e| raw_to_parse_error(&map, e).to_string())?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
#[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 {
|
|
||||||
lexer: Lexer::new(&file).peekmore(),
|
|
||||||
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(())
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
#[cfg(not(feature = "wasm"))]
|
|
||||||
pub fn from_path(p: &str) -> SassResult<String> {
|
|
||||||
let mut map = CodeMap::new();
|
|
||||||
let file = map.add_file(p.clone().into(), String::from_utf8(fs::read(p.clone())?)?);
|
|
||||||
Css::from_stylesheet(StyleSheet(
|
|
||||||
StyleSheetParser {
|
|
||||||
lexer: Lexer::new(&file).peekmore(),
|
|
||||||
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 {
|
|
||||||
lexer: Lexer::new(&file).peekmore(),
|
|
||||||
nesting: 0,
|
|
||||||
map,
|
|
||||||
path: p.as_ref(),
|
|
||||||
}
|
|
||||||
.parse_toplevel()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_stmts(s: Vec<Spanned<Stmt>>) -> StyleSheet {
|
|
||||||
StyleSheet(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StyleSheetParser<'a> {
|
|
||||||
lexer: PeekMoreIterator<Lexer<'a>>,
|
|
||||||
nesting: u32,
|
|
||||||
map: &'a mut CodeMap,
|
|
||||||
path: &'a Path,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
'a'..='z' | 'A'..='Z' | '_' | '-' | '0'..='9'
|
|
||||||
| '[' | '#' | ':' | '*' | '%' | '.' | '>' => rules
|
|
||||||
.extend(self.eat_rules(&Selector::new(), &mut Scope::new())?),
|
|
||||||
&'\t' | &'\n' | ' ' => {
|
|
||||||
self.lexer.next();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
'$' => {
|
|
||||||
self.lexer.next();
|
|
||||||
let name = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
|
||||||
devour_whitespace(&mut self.lexer);
|
|
||||||
let Token { kind, pos } = self
|
|
||||||
.lexer
|
|
||||||
.next()
|
|
||||||
.unwrap();
|
|
||||||
if kind != ':' {
|
|
||||||
return Err(("expected \":\".", pos).into());
|
|
||||||
}
|
|
||||||
let VariableDecl { val, default, .. } =
|
|
||||||
eat_variable_value(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
|
||||||
if !(default && global_var_exists(&name)) {
|
|
||||||
insert_global_var(&name.node, val)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'/' => {
|
|
||||||
self.lexer.next();
|
|
||||||
if '*' == self.lexer.peek().unwrap().kind {
|
|
||||||
self.lexer.next();
|
|
||||||
let comment = eat_comment(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
|
||||||
rules.push(comment.map_node(Stmt::MultilineComment));
|
|
||||||
} else if '/' == self.lexer.peek().unwrap().kind {
|
|
||||||
read_until_newline(&mut self.lexer);
|
|
||||||
devour_whitespace(&mut self.lexer);
|
|
||||||
} else {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'@' => {
|
|
||||||
self.lexer.next();
|
|
||||||
let Spanned { node: at_rule_kind, span } = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
|
||||||
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(
|
|
||||||
&mut self.lexer,
|
|
||||||
&Scope::new(),
|
|
||||||
&Selector::new(),
|
|
||||||
None,
|
|
||||||
)?),
|
|
||||||
AtRuleKind::Import => {
|
|
||||||
devour_whitespace(&mut self.lexer);
|
|
||||||
let mut file_name = String::new();
|
|
||||||
let next = match self.lexer.next() {
|
|
||||||
Some(v) => v,
|
|
||||||
None => todo!("expected input after @import")
|
|
||||||
};
|
|
||||||
match next.kind {
|
|
||||||
q @ '"' | q @ '\'' => {
|
|
||||||
file_name.push_str(
|
|
||||||
&parse_quoted_string(
|
|
||||||
&mut self.lexer,
|
|
||||||
&Scope::new(),
|
|
||||||
q,
|
|
||||||
&Selector::new())?
|
|
||||||
.node.unquote().to_css_string(span)?);
|
|
||||||
}
|
|
||||||
_ => return Err(("Expected string.", next.pos()).into()),
|
|
||||||
}
|
|
||||||
if let Some(t) = self.lexer.peek() {
|
|
||||||
if t.kind == ';' {
|
|
||||||
self.lexer.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devour_whitespace(&mut self.lexer);
|
|
||||||
|
|
||||||
let (new_rules, new_scope) = import(self.path, file_name.as_ref(), &mut self.map)?;
|
|
||||||
rules.extend(new_rules);
|
|
||||||
GLOBAL_SCOPE.with(|s| {
|
|
||||||
s.borrow_mut().extend(new_scope);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
let rule = AtRule::from_tokens(&v, span, &mut self.lexer, &mut Scope::new(), &Selector::new(), None)?;
|
|
||||||
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)?)
|
|
||||||
}
|
|
||||||
AtRule::Include(s) => rules.extend(s),
|
|
||||||
AtRule::Content => return Err(
|
|
||||||
("@content is only allowed within mixin declarations.", rule.span
|
|
||||||
).into()),
|
|
||||||
AtRule::If(i) => {
|
|
||||||
rules.extend(i.eval(&mut Scope::new(), &Selector::new(), None)?);
|
|
||||||
}
|
|
||||||
AtRule::AtRoot(root_rules) => rules.extend(root_rules),
|
|
||||||
AtRule::Unknown(..) => rules.push(rule.map_node(Stmt::AtRule)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'&' => {
|
|
||||||
return Err(
|
|
||||||
("Base-level rules cannot contain the parent-selector-referencing character '&'.", self.lexer.next().unwrap().pos()).into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
c if c.is_control() => {
|
|
||||||
return Err(("expected selector.", self.lexer.next().unwrap().pos()).into());
|
|
||||||
}
|
|
||||||
_ => 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();
|
|
||||||
while let Some(expr) = eat_expr(&mut self.lexer, scope, super_selector, None)? {
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn eat_expr<I: Iterator<Item = Token>>(
|
pub(crate) fn eat_expr<I: Iterator<Item = Token>>(
|
||||||
toks: &mut PeekMoreIterator<I>,
|
toks: &mut PeekMoreIterator<I>,
|
||||||
scope: &mut Scope,
|
scope: &mut Scope,
|
||||||
@ -736,27 +383,3 @@ pub(crate) fn eat_expr<I: Iterator<Item = Token>>(
|
|||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
398
src/stylesheet.rs
Normal file
398
src/stylesheet.rs
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
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::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,
|
||||||
|
read_until_newline, VariableDecl,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
map: &map,
|
||||||
|
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())?
|
||||||
|
.pretty_print()
|
||||||
|
.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(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[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 {
|
||||||
|
lexer: Lexer::new(&file).peekmore(),
|
||||||
|
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(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
pub fn from_path(p: &str) -> SassResult<String> {
|
||||||
|
let mut map = CodeMap::new();
|
||||||
|
let file = map.add_file(p.clone().into(), String::from_utf8(fs::read(p.clone())?)?);
|
||||||
|
Css::from_stylesheet(StyleSheet(
|
||||||
|
StyleSheetParser {
|
||||||
|
lexer: Lexer::new(&file).peekmore(),
|
||||||
|
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 {
|
||||||
|
lexer: Lexer::new(&file).peekmore(),
|
||||||
|
nesting: 0,
|
||||||
|
map,
|
||||||
|
path: p.as_ref(),
|
||||||
|
}
|
||||||
|
.parse_toplevel()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_stmts(s: Vec<Spanned<Stmt>>) -> StyleSheet {
|
||||||
|
StyleSheet(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StyleSheetParser<'a> {
|
||||||
|
lexer: PeekMoreIterator<Lexer<'a>>,
|
||||||
|
nesting: u32,
|
||||||
|
map: &'a mut CodeMap,
|
||||||
|
path: &'a Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
'a'..='z' | 'A'..='Z' | '_' | '-' | '0'..='9'
|
||||||
|
| '[' | '#' | ':' | '*' | '%' | '.' | '>' => rules
|
||||||
|
.extend(self.eat_rules(&Selector::new(), &mut Scope::new())?),
|
||||||
|
&'\t' | &'\n' | ' ' => {
|
||||||
|
self.lexer.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
'$' => {
|
||||||
|
self.lexer.next();
|
||||||
|
let name = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
||||||
|
devour_whitespace(&mut self.lexer);
|
||||||
|
let Token { kind, pos } = self
|
||||||
|
.lexer
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
if kind != ':' {
|
||||||
|
return Err(("expected \":\".", pos).into());
|
||||||
|
}
|
||||||
|
let VariableDecl { val, default, .. } =
|
||||||
|
eat_variable_value(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
||||||
|
if !(default && global_var_exists(&name)) {
|
||||||
|
insert_global_var(&name.node, val)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'/' => {
|
||||||
|
self.lexer.next();
|
||||||
|
if '*' == self.lexer.peek().unwrap().kind {
|
||||||
|
self.lexer.next();
|
||||||
|
let comment = eat_comment(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
||||||
|
rules.push(comment.map_node(Stmt::MultilineComment));
|
||||||
|
} else if '/' == self.lexer.peek().unwrap().kind {
|
||||||
|
read_until_newline(&mut self.lexer);
|
||||||
|
devour_whitespace(&mut self.lexer);
|
||||||
|
} else {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@' => {
|
||||||
|
self.lexer.next();
|
||||||
|
let Spanned { node: at_rule_kind, span } = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?;
|
||||||
|
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(
|
||||||
|
&mut self.lexer,
|
||||||
|
&Scope::new(),
|
||||||
|
&Selector::new(),
|
||||||
|
None,
|
||||||
|
)?),
|
||||||
|
AtRuleKind::Import => {
|
||||||
|
devour_whitespace(&mut self.lexer);
|
||||||
|
let mut file_name = String::new();
|
||||||
|
let next = match self.lexer.next() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => todo!("expected input after @import")
|
||||||
|
};
|
||||||
|
match next.kind {
|
||||||
|
q @ '"' | q @ '\'' => {
|
||||||
|
file_name.push_str(
|
||||||
|
&parse_quoted_string(
|
||||||
|
&mut self.lexer,
|
||||||
|
&Scope::new(),
|
||||||
|
q,
|
||||||
|
&Selector::new())?
|
||||||
|
.node.unquote().to_css_string(span)?);
|
||||||
|
}
|
||||||
|
_ => return Err(("Expected string.", next.pos()).into()),
|
||||||
|
}
|
||||||
|
if let Some(t) = self.lexer.peek() {
|
||||||
|
if t.kind == ';' {
|
||||||
|
self.lexer.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devour_whitespace(&mut self.lexer);
|
||||||
|
|
||||||
|
let (new_rules, new_scope) = import(self.path, file_name.as_ref(), &mut self.map)?;
|
||||||
|
rules.extend(new_rules);
|
||||||
|
GLOBAL_SCOPE.with(|s| {
|
||||||
|
s.borrow_mut().extend(new_scope);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
v => {
|
||||||
|
let rule = AtRule::from_tokens(&v, span, &mut self.lexer, &mut Scope::new(), &Selector::new(), None)?;
|
||||||
|
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)?)
|
||||||
|
}
|
||||||
|
AtRule::Include(s) => rules.extend(s),
|
||||||
|
AtRule::Content => return Err(
|
||||||
|
("@content is only allowed within mixin declarations.", rule.span
|
||||||
|
).into()),
|
||||||
|
AtRule::If(i) => {
|
||||||
|
rules.extend(i.eval(&mut Scope::new(), &Selector::new(), None)?);
|
||||||
|
}
|
||||||
|
AtRule::AtRoot(root_rules) => rules.extend(root_rules),
|
||||||
|
AtRule::Unknown(..) => rules.push(rule.map_node(Stmt::AtRule)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'&' => {
|
||||||
|
return Err(
|
||||||
|
("Base-level rules cannot contain the parent-selector-referencing character '&'.", self.lexer.next().unwrap().pos()).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
c if c.is_control() => {
|
||||||
|
return Err(("expected selector.", self.lexer.next().unwrap().pos()).into());
|
||||||
|
}
|
||||||
|
_ => 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();
|
||||||
|
while let Some(expr) = eat_expr(&mut self.lexer, scope, super_selector, None)? {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user