grass/src/lib.rs

387 lines
12 KiB
Rust
Raw Normal View History

/*! # grass
An implementation of the Sass specification in pure rust.
2020-07-24 23:49:26 -04:00
Spec progress as of 2020-07-24:
| Passing | Failing | Total |
|---------|---------|-------|
2020-07-24 23:49:26 -04:00
| 2935 | 2158 | 5093 |
## Use as library
```
2020-06-26 08:03:43 -04:00
fn main() -> Result<(), Box<grass::Error>> {
let sass = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?;
assert_eq!(sass, "a b {\n color: a b;\n}\n");
Ok(())
}
```
## Use as binary
```bash
cargo install grass
grass input.scss
```
*/
2020-03-01 09:08:13 -05:00
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![deny(missing_debug_implementations)]
#![allow(
// explicit return makes some things look ugly
clippy::implicit_return,
clippy::use_self,
clippy::missing_docs_in_private_items,
clippy::unreachable,
2020-07-03 12:56:19 -04:00
// this disallows binding as well, e.g. `v => ...`
clippy::wildcard_enum_match_arm,
2020-02-02 10:27:08 -05:00
clippy::module_name_repetitions,
2020-07-03 12:56:19 -04:00
// it is sometimes useful to break up `impl`s
2020-02-08 17:03:43 -05:00
clippy::multiple_inherent_impl,
2020-05-24 17:41:24 -04:00
// filter isn't fallible
clippy::filter_map,
clippy::else_if_without_else,
2020-05-31 05:32:19 -04:00
clippy::new_ret_no_self,
2020-06-07 23:11:43 -04:00
renamed_and_removed_lints,
clippy::unknown_clippy_lints,
clippy::replace_consts,
2020-06-24 07:05:14 -04:00
clippy::single_match,
2020-07-26 21:22:10 -04:00
clippy::float_arithmetic,
2020-03-30 15:43:15 -04:00
2020-02-14 18:28:09 -05:00
// temporarily allowed while under heavy development.
// eventually these allows should be refactored away
// to no longer be necessary
clippy::as_conversions,
clippy::todo,
clippy::too_many_lines,
clippy::panic,
2020-05-31 15:48:11 -04:00
clippy::unwrap_used,
clippy::option_unwrap_used,
2020-02-14 18:28:09 -05:00
clippy::result_unwrap_used,
clippy::cast_possible_truncation,
clippy::single_match_else,
clippy::indexing_slicing,
2020-04-21 18:22:26 -04:00
clippy::redundant_pub_crate,
2020-07-03 12:56:19 -04:00
// the api is changing too often to allot this
clippy::missing_errors_doc,
2020-05-31 05:32:19 -04:00
2020-07-03 12:56:19 -04:00
clippy::integer_arithmetic,
2020-05-31 05:32:19 -04:00
clippy::string_add,
clippy::get_unwrap,
clippy::wrong_self_convention,
clippy::items_after_statements,
clippy::shadow_reuse,
clippy::shadow_unrelated,
2020-07-08 22:38:56 -04:00
// this is only available on nightly
clippy::unnested_or_patterns,
)]
#![cfg_attr(feature = "nightly", feature(track_caller))]
2020-05-01 15:43:43 -04:00
#![cfg_attr(feature = "profiling", inline(never))]
2020-07-25 19:22:12 -04:00
use std::{collections::HashMap, fs, path::Path};
2020-06-16 20:40:19 -04:00
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
pub(crate) use beef::lean::Cow;
2020-06-16 19:38:30 -04:00
use codemap::CodeMap;
2020-04-12 19:37:12 -04:00
2020-06-16 19:38:30 -04:00
use peekmore::PeekMore;
2020-06-16 19:38:30 -04:00
pub use crate::error::{SassError as Error, SassResult as Result};
2020-03-29 13:28:17 -04:00
pub(crate) use crate::token::Token;
2020-06-16 19:38:30 -04:00
use crate::{
lexer::Lexer,
output::Css,
2020-07-05 10:13:49 -04:00
parse::{
common::{ContextFlags, NeverEmptyVec},
Parser,
},
2020-07-08 14:51:04 -04:00
scope::{Scope, Scopes},
2020-06-18 16:56:03 -04:00
selector::{Extender, Selector},
2020-03-29 13:28:17 -04:00
};
2020-01-25 11:00:29 -05:00
mod args;
2020-01-25 12:43:07 -05:00
mod atrule;
2020-01-25 20:58:30 -05:00
mod builtin;
mod color;
mod common;
mod error;
2020-07-08 22:38:56 -04:00
mod interner;
mod lexer;
mod output;
2020-06-16 19:38:30 -04:00
mod parse;
mod scope;
mod selector;
mod style;
2020-03-19 19:32:11 -04:00
mod token;
mod unit;
mod utils;
2020-01-25 09:58:53 -05:00
mod value;
#[non_exhaustive]
#[derive(Debug)]
pub enum OutputStyle {
/// The default style, this mode writes each
/// selector and declaration on its own line.
Expanded,
/// Ideal for release builds, this mode removes
/// as many extra characters as possible and
/// writes the entire stylesheet on a single line.
Compressed,
}
/// Configuration for Sass compilation
///
/// The simplest usage is `grass::Options::default()`;
/// however, a builder pattern is also exposed to offer
/// more control.
#[derive(Debug)]
pub struct Options<'a> {
2020-07-15 13:40:39 -04:00
style: OutputStyle,
load_paths: Vec<&'a Path>,
allows_charset: bool,
unicode_error_messages: bool,
quiet: bool,
}
impl<'a> Default for Options<'a> {
#[inline]
fn default() -> Self {
Self {
style: OutputStyle::Expanded,
load_paths: Vec::new(),
allows_charset: true,
unicode_error_messages: true,
quiet: false,
}
}
}
2020-07-15 13:42:18 +01:00
#[allow(clippy::missing_const_for_fn)]
impl<'a> Options<'a> {
/// `grass` currently offers 2 different output styles
///
/// - `OutputStyle::Expanded` writes each selector and declaration on its own line.
/// - `OutputStyle::Compressed` removes as many extra characters as possible
2020-07-15 13:40:39 -04:00
/// and writes the entire stylesheet on a single line.
///
/// By default, output is expanded.
2020-07-15 13:42:18 +01:00
#[must_use]
#[inline]
pub fn style(mut self, style: OutputStyle) -> Self {
self.style = style;
self
}
2020-07-15 13:40:39 -04:00
/// This flag tells Sass not to emit any warnings
/// when compiling. By default, Sass emits warnings
/// when deprecated features are used or when the
/// `@warn` rule is encountered. It also silences the
/// `@debug` rule.
///
/// By default, this value is `false` and warnings are emitted.
2020-07-15 13:42:18 +01:00
#[must_use]
#[inline]
pub fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
/// All Sass implementations allow users to provide
/// load paths: paths on the filesystem that Sass
/// will look in when locating modules. For example,
/// if you pass `node_modules/susy/sass` as a load path,
/// you can use `@import "susy"` to load `node_modules/susy/sass/susy.scss`.
///
/// Imports will always be resolved relative to the current
/// file first, though. Load paths will only be used if no
/// relative file exists that matches the module's URL. This
/// ensures that you can't accidentally mess up your relative
/// imports when you add a new library.
///
/// This method will append a single path to the list.
2020-07-15 13:42:18 +01:00
#[must_use]
#[inline]
pub fn load_path(mut self, path: &'a Path) -> Self {
self.load_paths.push(path);
self
}
/// Append multiple loads paths
///
/// Note that this method does *not* remove existing load paths
///
/// See [`Options::load_path`](Options::load_path) for more information about load paths
2020-07-15 13:42:18 +01:00
#[must_use]
#[inline]
pub fn load_paths(mut self, paths: &'a [&'a Path]) -> Self {
self.load_paths.extend_from_slice(paths);
self
}
2020-07-15 13:40:39 -04:00
/// This flag tells Sass whether to emit a `@charset`
/// declaration or a UTF-8 byte-order mark.
///
/// By default, Sass will insert either a `@charset`
/// declaration (in expanded output mode) or a byte-order
/// mark (in compressed output mode) if the stylesheet
/// contains any non-ASCII characters.
2020-07-15 13:42:18 +01:00
#[must_use]
#[inline]
pub fn allows_charset(mut self, allows_charset: bool) -> Self {
self.allows_charset = allows_charset;
self
}
/// This flag tells Sass only to emit ASCII characters as
/// part of error messages.
///
/// By default Sass will emit non-ASCII characters for
/// these messages.
///
/// This flag does not affect the CSS output.
2020-07-15 13:42:18 +01:00
#[must_use]
#[inline]
pub fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self {
self.unicode_error_messages = unicode_error_messages;
self
}
}
fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
2020-06-16 19:38:30 -04:00
let (message, span) = err.raw();
Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
2020-04-12 19:37:12 -04:00
}
2020-07-06 10:24:29 -04:00
/// Compile CSS from a path
///
2020-06-16 19:38:30 -04:00
/// ```
2020-06-26 06:40:34 -04:00
/// fn main() -> Result<(), Box<grass::Error>> {
/// let sass = grass::from_path("input.scss", &grass::Options::default())?;
2020-06-16 19:38:30 -04:00
/// Ok(())
/// }
/// ```
2020-07-06 10:24:29 -04:00
/// (grass does not currently allow files or paths that are not valid UTF-8)
2020-06-16 19:38:30 -04:00
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))]
2020-07-15 13:42:18 +01:00
pub fn from_path(p: &str, options: &Options) -> Result<String> {
2020-06-16 19:38:30 -04:00
let mut map = CodeMap::new();
let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?);
2020-06-22 12:39:09 -04:00
let empty_span = file.span.subspan(0, 0);
let stmts = Parser {
toks: &mut Lexer::new(&file)
.collect::<Vec<Token>>()
.into_iter()
.peekmore(),
map: &mut map,
path: p.as_ref(),
2020-07-08 14:51:04 -04:00
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span,
content: &mut Vec::new(),
2020-07-05 10:13:49 -04:00
flags: ContextFlags::empty(),
at_root: true,
at_root_has_selector: false,
extender: &mut Extender::new(empty_span),
2020-07-08 17:52:37 -04:00
content_scopes: &mut Scopes::new(),
options,
2020-07-25 19:22:12 -04:00
modules: &mut HashMap::new(),
}
.parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
Css::from_stmts(stmts, false, options.allows_charset)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?
.pretty_print(&map)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
}
2020-07-06 10:24:29 -04:00
/// Compile CSS from a string
2020-06-16 19:38:30 -04:00
///
/// ```
2020-06-26 06:40:34 -04:00
/// fn main() -> Result<(), Box<grass::Error>> {
/// let sass = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?;
2020-06-16 19:38:30 -04:00
/// assert_eq!(sass, "a b {\n color: a b;\n}\n");
/// Ok(())
/// }
/// ```
#[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)]
2020-06-16 20:40:19 -04:00
#[cfg(not(feature = "wasm"))]
2020-07-15 13:42:18 +01:00
pub fn from_string(p: String, options: &Options) -> Result<String> {
2020-06-16 19:38:30 -04:00
let mut map = CodeMap::new();
let file = map.add_file("stdin".into(), p);
2020-06-22 12:39:09 -04:00
let empty_span = file.span.subspan(0, 0);
let stmts = Parser {
toks: &mut Lexer::new(&file)
.collect::<Vec<Token>>()
.into_iter()
.peekmore(),
map: &mut map,
path: Path::new(""),
2020-07-08 14:51:04 -04:00
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span,
content: &mut Vec::new(),
2020-07-05 10:13:49 -04:00
flags: ContextFlags::empty(),
at_root: true,
at_root_has_selector: false,
extender: &mut Extender::new(empty_span),
2020-07-08 17:52:37 -04:00
content_scopes: &mut Scopes::new(),
options,
2020-07-25 19:22:12 -04:00
modules: &mut HashMap::new(),
}
.parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
Css::from_stmts(stmts, false, options.allows_charset)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?
.pretty_print(&map)
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
}
2020-06-16 20:40:19 -04:00
#[cfg(feature = "wasm")]
#[wasm_bindgen]
2020-07-24 23:58:26 -04:00
pub fn from_string(p: String) -> std::result::Result<String, JsValue> {
2020-06-16 20:40:19 -04:00
let mut map = CodeMap::new();
let file = map.add_file("stdin".into(), p);
2020-06-22 12:39:09 -04:00
let empty_span = file.span.subspan(0, 0);
let stmts = Parser {
toks: &mut Lexer::new(&file)
.collect::<Vec<Token>>()
.into_iter()
.peekmore(),
map: &mut map,
path: Path::new(""),
2020-07-08 14:51:04 -04:00
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span,
content: &mut Vec::new(),
2020-07-05 10:13:49 -04:00
flags: ContextFlags::empty(),
at_root: true,
at_root_has_selector: false,
extender: &mut Extender::new(empty_span),
2020-07-08 17:52:37 -04:00
content_scopes: &mut Scopes::new(),
2020-07-24 23:58:26 -04:00
options: &Options::default(),
2020-07-25 19:22:12 -04:00
modules: &mut HashMap::new(),
}
.parse()
2020-07-24 23:58:26 -04:00
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?;
2020-07-24 23:58:26 -04:00
Ok(Css::from_stmts(stmts, false, true)
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?
.pretty_print(&map)
2020-07-24 23:58:26 -04:00
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?)
2020-06-16 20:40:19 -04:00
}