grass_compiler/
lib.rs

1/*!
2This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS.
3
4This crate targets compatibility with the reference implementation in Dart. If
5upgrading from the [now deprecated](https://sass-lang.com/blog/libsass-is-deprecated)
6`libsass`, one may have to modify their stylesheets. These changes will not differ
7from those necessary to upgrade to `dart-sass`, and in general such changes should
8be quite rare.
9
10This crate is capable of compiling Bootstrap 4 and 5, bulma and bulma-scss, Bourbon,
11as well as most other large Sass libraries with complete accuracy. For the vast
12majority of use cases there should be no perceptible differences from the reference
13implementation.
14
15## Use as library
16```
17# use grass_compiler as grass;
18fn main() -> Result<(), Box<grass::Error>> {
19    let css = grass::from_string(
20        "a { b { color: &; } }".to_owned(),
21        &grass::Options::default().style(grass::OutputStyle::Compressed)
22    )?;
23    assert_eq!(css, "a b{color:a b}");
24    Ok(())
25}
26```
27
28## Use as binary
29```bash
30cargo install grass
31grass input.scss
32```
33*/
34
35#![cfg_attr(doc_cfg, feature(doc_cfg))]
36#![warn(clippy::all, clippy::cargo, clippy::dbg_macro)]
37#![deny(missing_debug_implementations)]
38#![allow(
39    clippy::use_self,
40    // filter isn't fallible
41    clippy::manual_filter_map,
42    renamed_and_removed_lints,
43    clippy::unknown_clippy_lints,
44    clippy::single_match,
45    clippy::new_without_default,
46    clippy::single_match_else,
47    clippy::multiple_crate_versions,
48    clippy::wrong_self_convention,
49    clippy::comparison_chain,
50    clippy::unwrap_or_default,
51    clippy::manual_unwrap_or_default,
52
53    // todo: these should be enabled
54    clippy::arc_with_non_send_sync,
55
56    // todo: unignore once we bump MSRV
57    clippy::assigning_clones,
58
59    unknown_lints,
60)]
61
62use std::path::Path;
63
64use parse::{CssParser, SassParser, StylesheetParser};
65use sass_ast::StyleSheet;
66use serializer::Serializer;
67#[cfg(feature = "wasm-exports")]
68use wasm_bindgen::prelude::*;
69
70use codemap::CodeMap;
71
72pub use crate::error::{
73    PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
74};
75pub use crate::fs::{Fs, NullFs, StdFs};
76pub use crate::logger::{Logger, NullLogger, StdLogger};
77pub use crate::options::{InputSyntax, Options, OutputStyle};
78pub use crate::{builtin::Builtin, evaluate::Visitor};
79pub(crate) use crate::{context_flags::ContextFlags, lexer::Token};
80use crate::{lexer::Lexer, parse::ScssParser};
81
82pub mod sass_value {
83    pub use crate::{
84        ast::ArgumentResult,
85        color::Color,
86        common::{BinaryOp, Brackets, ListSeparator, QuoteKind},
87        unit::{ComplexUnit, Unit},
88        value::{
89            ArgList, CalculationArg, CalculationName, Number, SassCalculation, SassFunction,
90            SassMap, SassNumber, Value,
91        },
92    };
93}
94
95pub mod sass_ast {
96    pub use crate::ast::*;
97}
98
99pub use codemap;
100
101mod ast;
102mod builtin;
103mod color;
104mod common;
105mod context_flags;
106mod error;
107mod evaluate;
108mod fs;
109mod interner;
110mod lexer;
111mod logger;
112mod options;
113mod parse;
114mod selector;
115mod serializer;
116mod unit;
117mod utils;
118mod value;
119
120fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
121    let (message, span) = err.raw();
122    Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
123}
124
125pub fn parse_stylesheet<P: AsRef<Path>>(
126    input: String,
127    file_name: P,
128    options: &Options,
129) -> Result<StyleSheet> {
130    // todo: much of this logic is duplicated in `from_string_with_file_name`
131    let mut map = CodeMap::new();
132    let path = file_name.as_ref();
133    let file = map.add_file(path.to_string_lossy().into_owned(), input);
134    let empty_span = file.span.subspan(0, 0);
135    let lexer = Lexer::new_from_file(&file);
136
137    let input_syntax = options
138        .input_syntax
139        .unwrap_or_else(|| InputSyntax::for_path(path));
140
141    let stylesheet = match input_syntax {
142        InputSyntax::Scss => {
143            ScssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse()
144        }
145        InputSyntax::Sass => {
146            SassParser::new(lexer, options, empty_span, file_name.as_ref()).__parse()
147        }
148        InputSyntax::Css => {
149            CssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse()
150        }
151    };
152
153    let stylesheet = match stylesheet {
154        Ok(v) => v,
155        Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
156    };
157
158    Ok(stylesheet)
159}
160
161fn from_string_with_file_name<P: AsRef<Path>>(
162    input: String,
163    file_name: P,
164    options: &Options,
165) -> Result<String> {
166    let mut map = CodeMap::new();
167    let path = file_name.as_ref();
168    let file = map.add_file(path.to_string_lossy().into_owned(), input);
169    let empty_span = file.span.subspan(0, 0);
170    let lexer = Lexer::new_from_file(&file);
171
172    let input_syntax = options
173        .input_syntax
174        .unwrap_or_else(|| InputSyntax::for_path(path));
175
176    let stylesheet = match input_syntax {
177        InputSyntax::Scss => {
178            ScssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse()
179        }
180        InputSyntax::Sass => {
181            SassParser::new(lexer, options, empty_span, file_name.as_ref()).__parse()
182        }
183        InputSyntax::Css => {
184            CssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse()
185        }
186    };
187
188    let stylesheet = match stylesheet {
189        Ok(v) => v,
190        Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
191    };
192
193    let mut visitor = Visitor::new(path, options, &mut map, empty_span);
194    match visitor.visit_stylesheet(stylesheet) {
195        Ok(_) => {}
196        Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
197    }
198    let stmts = visitor.finish();
199
200    let mut serializer = Serializer::new(options, &map, false, empty_span);
201
202    let mut prev_was_group_end = false;
203    let mut prev_requires_semicolon = false;
204    for stmt in stmts {
205        if stmt.is_invisible() {
206            continue;
207        }
208
209        let is_group_end = stmt.is_group_end();
210        let requires_semicolon = Serializer::requires_semicolon(&stmt);
211
212        serializer
213            .visit_group(stmt, prev_was_group_end, prev_requires_semicolon)
214            .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
215
216        prev_was_group_end = is_group_end;
217        prev_requires_semicolon = requires_semicolon;
218    }
219
220    Ok(serializer.finish(prev_requires_semicolon))
221}
222
223/// Compile CSS from a path
224///
225/// n.b. `grass` does not currently support files or paths that are not valid UTF-8
226///
227/// ```
228/// # use grass_compiler as grass;
229/// fn main() -> Result<(), Box<grass::Error>> {
230///     let css = grass::from_path("input.scss", &grass::Options::default())?;
231///     Ok(())
232/// }
233/// ```
234#[inline]
235pub fn from_path<P: AsRef<Path>>(p: P, options: &Options) -> Result<String> {
236    from_string_with_file_name(String::from_utf8(options.fs.read(p.as_ref())?)?, p, options)
237}
238
239/// Compile CSS from a string
240///
241/// ```
242/// # use grass_compiler as grass;
243/// fn main() -> Result<(), Box<grass::Error>> {
244///     let css = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?;
245///     assert_eq!(css, "a b {\n  color: a b;\n}\n");
246///     Ok(())
247/// }
248/// ```
249#[inline]
250pub fn from_string<S: Into<String>>(input: S, options: &Options) -> Result<String> {
251    from_string_with_file_name(input.into(), "stdin", options)
252}
253
254#[cfg(feature = "wasm-exports")]
255#[wasm_bindgen(js_name = from_string)]
256pub fn from_string_js(input: String) -> std::result::Result<String, String> {
257    from_string(input, &Options::default()).map_err(|e| e.to_string())
258}