├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── docs ├── index.md └── types.md ├── examples └── full │ ├── main.fuss │ └── other.fuss └── src ├── cache.rs ├── cli.yml ├── errors ├── display.rs ├── errors.rs └── mod.rs ├── evaluator.rs ├── macros.rs ├── main.rs ├── outputter.rs ├── parser ├── grammar.pest ├── mod.rs └── parser.rs ├── prelude ├── blocks.rs ├── casting.rs ├── colour.rs ├── import.rs ├── mod.rs └── ops.rs └── types ├── colour.rs ├── list.rs ├── mod.rs ├── scope.rs ├── smallvec.rs └── types.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | target 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuss-rs" 3 | version = "0.1.0" 4 | authors = ["James Wilson "] 5 | 6 | [dependencies] 7 | pest = "^1.0" 8 | pest_derive = "^1.0" 9 | clap = {version = "2.26", features = ["yaml"]} -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 James Wilson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [Documentation](docs/index.md) 2 | - [Examples](examples) 3 | 4 | # FUSS - A Functional CSS Preprocessor. 5 | 6 | The goal of FUSS is to improve on the power to weight ratio of existing CSS preprocessors. The syntax aims to be a superset of CSS, so you can write CSS as you would normally, but augment it with FUSS syntax when you'd like the extra functionality. 7 | 8 | In FUSS, almost everything is an expression. Functions, variables, and CSS blocks themselves can all be composed with eachother. FUSS looks something like: 9 | 10 | ``` 11 | $other: import("other"); 12 | 13 | $drop: &.drop { 14 | border: 3px solid yellow; 15 | .icon { 16 | color: yellow; 17 | } 18 | }; 19 | 20 | .site-body { 21 | 22 | // apply our other theme here, 23 | // overriding the title colors: 24 | $other.theme({ 25 | $title: red; 26 | $titleBg: black; 27 | }); 28 | 29 | // add drop styling too: 30 | $drop; 31 | 32 | &.massive .banner, &.huge .banner { 33 | height: 100px; 34 | } 35 | 36 | } 37 | ``` 38 | 39 | Check out the [examples](examples) folder for more. 40 | 41 | ## Installation 42 | 43 | The easiest way to have a play with FUSS at the moment is to build from source. This is very straightforward: 44 | 45 | 1. install rust from [the official website](https://www.rust-lang.org). 46 | 2. run `cargo install` in this folder to build an optimised binary. 47 | 48 | ## Note 49 | 50 | This is a prototype/work in progress. Many things work great, but there is still much to do before it can cover the vast majority of use cases! 51 | 52 | ## Contributions 53 | 54 | Feedback and pull requests are welcome! Please open an issue prior to doing any serious amount of work so that we can have a chat and make sure we're on the same page :) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # FUSS - A Functional CSS Preprocessor. 2 | 3 | The goal of FUSS is to improve on the power to weight ratio of existing CSS preprocessors. The syntax aims to be a superset of CSS, so you can write CSS as you would normally, but augment it with FUSS syntax when you'd like the extra functionality. 4 | 5 | In FUSS, almost everything is an expression. Functions, variables, and CSS blocks themselves can all be composed with eachother. 6 | 7 | - [Types](types.md) 8 | - [Examples](../examples) -------------------------------------------------------------------------------- /docs/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | Expressions in FUSS are all one of the following types: 4 | 5 | ## Booleans 6 | 7 | FUSS supports `true` and `false` booleans, which come in handy for making comparisons in `if` expressions. 8 | 9 | ``` 10 | $t: true; 11 | $f: false; 12 | ``` 13 | 14 | ## Units 15 | 16 | Units are just numbers with an optional suffix. The following are all examples of valid units: 17 | 18 | ``` 19 | $unit: -1; 20 | $unit: 1.2; 21 | $unit: 10%; 22 | $unit: 27px; 23 | $unit: 200em; 24 | $unit: 0.75deg; 25 | ``` 26 | 27 | ## Strings 28 | 29 | Strings are useful for interpolating arbitrary content into CSS that FUSS otherwise does not properly understand. Strings are surrounded in double quotes. You need to escape any double quotes or backslashes that you'd like to use using a backslash. These are all valid: 30 | 31 | ``` 32 | $str: "hello"; 33 | $str: "hello \"world\""; 34 | $str: "back\\slashes\\need\\escaping\\too"; 35 | ``` 36 | 37 | All other characters are interpreted literally in a string. 38 | 39 | Strings can be interpolated almost anywhere; the following is all valid (noting that selectors and property names require a `${}` to interpolate FUSS expressions into them): 40 | 41 | ``` 42 | $myString: "awesome"; 43 | $prefix: "background"; 44 | $value: "a-made-up-" + "color-name"; 45 | 46 | ${ $myString }-class { 47 | ${ $prefix }-color: $value; 48 | } 49 | ``` 50 | 51 | ## Colours 52 | 53 | Colours are primaily declared in one of the following ways: 54 | 55 | ``` 56 | $col: blue; 57 | $col: rgb(0,0,255); 58 | $col: rgba(0,0,255,0.5); 59 | $col: hsl(240deg,100%,50%); 60 | $col: hsla(240deg,100%,50%, 0.5); 61 | ``` 62 | 63 | All of the colour names supported in CSS are also supported in FUSS. 64 | 65 | ## Variables 66 | 67 | Variables can be declared at the top level of a file, or inside a block. They don't have to be declared before being used, and they are scoped to the file or block in which they are declared (whichever is smaller in scope). 68 | 69 | Basic usage: 70 | 71 | ``` 72 | $b: blue; 73 | 74 | .hello { 75 | color: $b; 76 | } 77 | ``` 78 | 79 | This has exactly the same output: 80 | 81 | ``` 82 | .hello { 83 | color: $b; 84 | } 85 | 86 | $b: blue; 87 | ``` 88 | 89 | As does this: 90 | 91 | ``` 92 | .hello { 93 | $b: blue; // $b is not known about outside of .hello 94 | color: $b; 95 | } 96 | ``` 97 | 98 | ## If expressions 99 | 100 | Currently the only branching mechanism in FUSS; `if` expressions allow a different result to be returned based on the result of some condition. The condition evaluates to `false` if it's a 0 valued unit, the literal `false`, an empty string or `undefined`, and `true` otherwise. Usage: 101 | 102 | ``` 103 | $val: 100; 104 | // This evaluates to the string "yes": 105 | $yes: if $val >= 100 then "yes" else "no"; 106 | // This evaluates to the string "no": 107 | $no: if $val < 100 then "yes" else "no"; 108 | ``` 109 | 110 | ## Blocks 111 | 112 | Blocks are the core building block of CSS. Blocks optionally have a selector, and inside them we can declare variables, key-value pairs of CSS properties, or nest other blocks. One can also access variables declared inside blocks, making them useful as named arguments to functions. 113 | 114 | In their most basic form, blocks allow reuse of common key-value pairs: 115 | 116 | ``` 117 | $block: { 118 | color: blue; 119 | padding: 100px; 120 | }; 121 | 122 | .hello { 123 | $block; 124 | } 125 | .foo { 126 | $block; 127 | } 128 | ``` 129 | 130 | Blocks can also have their own selectors: 131 | 132 | ``` 133 | $block: .world { 134 | color: blue; 135 | padding: 100px; 136 | }; 137 | 138 | // outputs a blue, 100px ".hello .world" block: 139 | .hello { 140 | $block; 141 | } 142 | ``` 143 | 144 | We can declare variables and other blocks inside blocks, so this is equivalent to the above: 145 | 146 | ``` 147 | $block: .hello { 148 | $col: blue; 149 | .world { 150 | $pad: 100px; 151 | color: $col; 152 | padding: $pad; 153 | } 154 | }; 155 | 156 | $block; 157 | ``` 158 | 159 | We can pluck out the values of variables inside blocks as well: 160 | 161 | ``` 162 | $block:{ 163 | $a: { 164 | $b: 100px; 165 | }; 166 | $c: blue; 167 | }; 168 | 169 | .hello { 170 | padding: $block.a.b; 171 | colour: $block.c; 172 | } 173 | ``` 174 | 175 | A neat trick if you want to declare intermediate variables without exposing them outside of a function is to work inside a block and then return only what you want from it. Here, `$a` is 3: 176 | 177 | ``` 178 | $a: ({ 179 | // these internal variables are hidden outside this block: 180 | $a: 1px; 181 | $b: 2; 182 | // we access $res from the block to return it. Parens are 183 | // required around the block to make parsing simpler: 184 | $res: $a + $b; 185 | }).res; 186 | 187 | .hello { 188 | padding: $a; 189 | } 190 | ``` 191 | 192 | Passing and returning blocks is particularly useful within functions.. 193 | 194 | ## Functions 195 | 196 | A function in FUSS has access to variables defined in the enclosing blocks at the point of its definition, and can also take arguments. Some examples: 197 | 198 | ``` 199 | $fn: ($a) => if $a then $a + 2 else 100px; 200 | $fn: () => true; 201 | $fn: ($a, $b) => $a + $b * 2; 202 | ``` 203 | 204 | Functions can combine with blocks to allow for something like named arguments: 205 | 206 | ``` 207 | $fn: ($b) => { 208 | padding: $b.padding; 209 | color: $b.colour; 210 | border-radius: ${ if $b.border then $b.border else 5px }; 211 | margin: 5px; 212 | }; 213 | 214 | .hello { 215 | $fn({ $padding: 5px; $colour: red }); 216 | } 217 | ``` 218 | 219 | The above function both takes and returns a block. It allows for an optional $border argument to be provided, defaulting to a 5px border radius if not. 220 | 221 | ## Undefined 222 | 223 | `undefined` is a special value given back on trying to access properties or function arguments that have not been given a value. It cannot however be interpolated into CSS, preventing accidental misuse. `undefined` allows us to create functions which take optional arguments amoung other things. 224 | 225 | ``` 226 | $fn: ($val) => if $val == undefined then 100px else $val; 227 | 228 | .hello { 229 | padding: $fn(); // defaults to 100px. 230 | margin: $fn(200px); // 200px. 231 | } 232 | ``` -------------------------------------------------------------------------------- /examples/full/main.fuss: -------------------------------------------------------------------------------- 1 | $other: import("other"); 2 | 3 | $drop: &.drop { 4 | border: 3px solid yellow; 5 | .icon { 6 | color: yellow; 7 | } 8 | }; 9 | 10 | .site-body { 11 | 12 | // apply our other theme here, 13 | // overriding the title colors: 14 | $other.theme({ 15 | $title: red; 16 | $titleBg: black; 17 | }); 18 | 19 | // add drop styling too: 20 | $drop; 21 | 22 | &.massive .banner, &.huge .banner { 23 | height: 100px; 24 | } 25 | 26 | } 27 | 28 | .secondary-body { 29 | 30 | // apply our other theme again here, 31 | // but using the default values: 32 | $other.theme(); 33 | 34 | // add drop styling too: 35 | $drop; 36 | 37 | margin: 2em; 38 | 39 | } -------------------------------------------------------------------------------- /examples/full/other.fuss: -------------------------------------------------------------------------------- 1 | $theme: ($overrides) => { 2 | 3 | // merge default theme values with provided overrides: 4 | $settings: merge({ 5 | $title: gray; 6 | $titleBg: #ff0000; 7 | $text: #000; 8 | $bodyBg: rgb(255,255,255); 9 | $titleHeight: 50px; 10 | }, $overrides); 11 | 12 | // a helper for setting color/bgcolor: 13 | $colors: ($fg, $bg) => { 14 | color: $fg; 15 | background-color: $bg; 16 | }; 17 | 18 | .banner { 19 | height: $settings.titleHeight; 20 | $colors($settings.title, $settings.titleBg); 21 | } 22 | 23 | .main { 24 | padding: 15px; 25 | $colors($settings.text, $settings.bodyBg); 26 | } 27 | 28 | }; -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::rc::Rc; 3 | use std::cell::RefCell; 4 | use std::hash::Hash; 5 | 6 | /// A cache to be used in contexts: 7 | #[derive(Clone)] 8 | pub struct Cache( Rc>> ); 9 | 10 | impl Cache { 11 | pub fn new() -> Cache { 12 | Cache( Rc::new( RefCell::new(HashMap::new()) ) ) 13 | } 14 | pub fn set(&self, key: K, entry: V) { 15 | self.0.borrow_mut().insert(key, entry); 16 | } 17 | pub fn exists(&self, key: &K) -> bool { 18 | self.0.borrow().contains_key(key) 19 | } 20 | pub fn get(&self, key: &K) -> Option { 21 | self.0.borrow_mut().get(key).map(|o| o.clone()) 22 | } 23 | } -------------------------------------------------------------------------------- /src/cli.yml: -------------------------------------------------------------------------------- 1 | name: fuss 2 | version: "1.0" 3 | author: James W. 4 | about: FUSS, a CSS Preprocessor 5 | args: 6 | - input: 7 | short: i 8 | help: The root FUSS file to compile 9 | required: false 10 | takes_value: true -------------------------------------------------------------------------------- /src/errors/display.rs: -------------------------------------------------------------------------------- 1 | use errors::errors::*; 2 | use types::At; 3 | use std::path::{Path,PathBuf}; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::fmt::Write; 7 | use std::iter; 8 | use std::borrow::{Borrow,Cow}; 9 | 10 | pub struct Options<'a> { 11 | stdin: &'a str, 12 | tab_width: usize 13 | } 14 | impl <'a> Options<'a> { 15 | pub fn with_stdin(input: &str) -> Options { 16 | Options { stdin: input, tab_width: 4 } 17 | } 18 | } 19 | 20 | pub fn display_import_error<'a, E: Borrow, O: Borrow>>(e: E, opts: O) { 21 | use self::ImportError::*; 22 | match *e.borrow() { 23 | CannotImportNoPathSet | ImportLoop{..} => { 24 | unreachable!() 25 | }, 26 | CannotOpenFile(ref path) => { 27 | eprintln!("I can't open the file:\n\n{}\n\nDoes it exist?", path.display()) 28 | }, 29 | CannotReadFile(ref path) => { 30 | eprintln!("I can't read the file:\n\n{}\n\nPerhaps you do not have permission?", path.display()) 31 | }, 32 | CompileError(ref err, ref path) => { 33 | if *path == PathBuf::new() { 34 | eprintln!("I ran into an error compiling from stdin:\n\n{}" 35 | , error_string(err.borrow(), opts.borrow()) 36 | ) 37 | } else { 38 | eprintln!("I ran into an error compiling the file {}:\n\n{}" 39 | , path.display() 40 | , error_string(err.borrow(), opts.borrow()) 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | 47 | pub fn display_warning<'a, E: Borrow, O: Borrow>>(e: E, opts: O) { 48 | eprintln!("Warning: {}", error_string(e.borrow(), opts.borrow())); 49 | } 50 | 51 | pub fn display_error<'a, E: Borrow, O: Borrow>>(e: E, opts: O) { 52 | eprintln!("Error: {}", error_string(e.borrow(), opts.borrow())); 53 | } 54 | 55 | // context provides the current path of the file that the error happened in. 56 | // Each time we hit an import error, we recurse into it using the new path. 57 | fn error_string<'a>(err: &Error, opts: &Options<'a>) -> String { 58 | 59 | let mut out = match err.cause() { 60 | ErrorKind::ImportError(ImportError::CompileError(ref err, ..)) => { 61 | let mut o = error_string(err, opts); 62 | o.push_str("\n"); 63 | o 64 | }, 65 | ErrorKind::ContextError(ContextError::At(ref err)) => { 66 | let mut o = error_string(err, opts); 67 | o.push_str("\n"); 68 | o 69 | }, 70 | _ => { 71 | String::new() 72 | } 73 | }; 74 | 75 | let at = err.at(); 76 | let file_cow = if at.file() == &*PathBuf::new() && !opts.stdin.is_empty() { 77 | Cow::Borrowed(opts.stdin) 78 | } else { 79 | Cow::Owned(read_to_string(at.file()).unwrap_or(String::new())) 80 | }; 81 | 82 | out.push_str(&err.error_summary()); 83 | out.push_str(":\n\n"); 84 | out.push_str(&highlight_error(&at, file_cow.borrow(), opts.tab_width) 85 | .unwrap_or_else(|| at.file().display().to_string())); 86 | out.push('\n'); 87 | 88 | let desc = err.error_description(); 89 | if !desc.is_empty() { 90 | out.push_str(&desc); 91 | out.push_str(".\n"); 92 | } 93 | 94 | out 95 | } 96 | 97 | fn read_to_string>(path: P) -> Option { 98 | let mut file = String::new(); 99 | File::open(path).ok()?.read_to_string(&mut file).ok()?; 100 | Some(file) 101 | } 102 | 103 | // print the relevant part of the file with the error location highlighted: 104 | fn highlight_error(at: &At, file: &str, tab_width: usize) -> Option { 105 | 106 | let by_lines: Vec<&str> = file.lines().collect(); 107 | let Offsets{start_line, start_offset, end_line, end_offset} = get_lines_from_location(at.start(), at.end(), file); 108 | 109 | let max_line_num_length = (start_line+1..end_line+2).fold(0, |max,n| { 110 | max.max(n.to_string().len()) 111 | }); 112 | 113 | let mut out = String::new(); 114 | let line_num_spaces = spaces(max_line_num_length); 115 | 116 | writeln!(&mut out, "{}--> {} ({}:{}-{}:{})" 117 | , line_num_spaces 118 | , at.file().display() 119 | // display 1 indexed values for humans: 120 | , start_line+1, start_offset+1, end_line+1, end_offset+1 ).unwrap(); 121 | 122 | writeln!(&mut out, "{} |", line_num_spaces).unwrap(); 123 | for line in start_line..end_line+1 { 124 | let line_human = line+1; 125 | let num_str = padded_num(line_human, max_line_num_length); 126 | 127 | let raw_line_str = by_lines.get(line).unwrap_or(&""); 128 | let start_offset = adjust_offset_for_tabs(raw_line_str, start_offset, tab_width); 129 | let end_offset = adjust_offset_for_tabs(raw_line_str, end_offset, tab_width); 130 | let line_string = tabs_to_spaces(raw_line_str, tab_width); 131 | 132 | writeln!(&mut out, "{} | {}", num_str, line_string).unwrap(); 133 | 134 | if line == start_line { 135 | // cater for start and end offset being on same line, and for start offset 136 | // being at the end of the line (past it): 137 | let n = if start_line == end_line { end_offset.checked_sub(start_offset).unwrap_or(0).max(1) } 138 | else { line_string.len().checked_sub(start_offset).unwrap_or(0).max(1) }; 139 | 140 | let arrows: String = iter::repeat('^').take(n).collect(); 141 | writeln!(&mut out, "{} | {}{}", line_num_spaces, spaces(start_offset), arrows).unwrap(); 142 | } else if line > start_line && line < end_line { 143 | let arrows: String = iter::repeat('^').take(line_string.len()).collect(); 144 | writeln!(&mut out, "{} | {}", line_num_spaces, arrows).unwrap(); 145 | } else if line == end_line { 146 | // cater for position being off the end of the line. 147 | let n = line_string.len().checked_sub(end_offset).unwrap_or(0).max(1); 148 | let arrows: String = iter::repeat('^').take(n).collect(); 149 | writeln!(&mut out, "{} | {}", line_num_spaces, arrows).unwrap(); 150 | } 151 | } 152 | 153 | Some(out) 154 | } 155 | 156 | // adjust some offset for a line to take into account tabs of some width. 157 | fn adjust_offset_for_tabs(line: &str, offset: usize, tab_width: usize) -> usize { 158 | let tab_count = line.as_bytes().iter().take(offset).filter(|&&b| b == b'\t').count(); 159 | offset - tab_count + (tab_count * tab_width) 160 | } 161 | 162 | // swap tabs for spaces in some input 163 | fn tabs_to_spaces(line: &str, tab_width: usize) -> String { 164 | let s = spaces(tab_width); 165 | line.replace('\t', &s) 166 | } 167 | 168 | // returns a String consisting of n spaces 169 | fn spaces(n: usize) -> String { 170 | iter::repeat(' ').take(n).collect() 171 | } 172 | 173 | // left-pad a number out to ensure the resulting string is always len in size 174 | fn padded_num(num: usize, len: usize) -> String { 175 | let num_str = num.to_string(); 176 | let num_len = num_str.len(); 177 | if num_len < len { 178 | format!("{}{}", spaces(len - num_len), num_str) 179 | } else { 180 | num_str 181 | } 182 | } 183 | 184 | // given a start and end byte offset, we give back start and end line counts 185 | // and offsets. 186 | fn get_lines_from_location(start: usize, end: usize, file: &str) -> Offsets { 187 | 188 | let mut start_line = 0; 189 | let mut start_offset = 0; 190 | let mut end_line = 0; 191 | let mut end_offset = 0; 192 | let mut last_newline = 0; 193 | let mut lines_seen = 0; 194 | for (n, c) in file.char_indices().chain(iter::once((file.len(),' '))) { 195 | 196 | if n == start { 197 | start_line = lines_seen; 198 | start_offset = n - last_newline - 1; 199 | } 200 | if n == end { 201 | end_line = lines_seen; 202 | end_offset = n - last_newline - 1; 203 | break; 204 | } 205 | 206 | if c == '\n' { 207 | last_newline = n; 208 | lines_seen += 1; 209 | } 210 | } 211 | 212 | Offsets{start_line, start_offset, end_line, end_offset} 213 | } 214 | 215 | struct Offsets { 216 | start_line: usize, 217 | start_offset: usize, 218 | end_line: usize, 219 | end_offset: usize 220 | } 221 | -------------------------------------------------------------------------------- /src/errors/errors.rs: -------------------------------------------------------------------------------- 1 | use std::path::{PathBuf}; 2 | use std::iter; 3 | use std::convert::Into; 4 | use types::{EvaluatedExpr,VarType,Kind,At,SmallVec}; 5 | use parser::parser::Rule; 6 | 7 | // Usage: 8 | // import errors::*; 9 | // err(ApplicationError::NotAFunction, At::position(somefile, 0,100)) 10 | // 11 | // An error has a position and an underlying cause. Anything that 12 | // can be turned into a ErrorKind (including this) can be wrapped in 13 | // this struct, to build up a stack trace. It's intended that one 14 | // creates an errror by running errors::new given a Position and an 15 | // errors::Shape/Application etc, or a pre-existing Error. 16 | #[derive(Clone,PartialEq,Debug)] 17 | pub struct Error { 18 | at: SmallVec, 19 | cause: Box 20 | } 21 | impl ErrorText for Error { 22 | fn error_summary(&self) -> String { 23 | self.cause.error_summary() 24 | } 25 | fn error_description(&self) -> String { 26 | self.cause.error_description() 27 | } 28 | } 29 | impl Error { 30 | pub fn new, L: Into>>(err: E, pos: L) -> Error { 31 | Error { 32 | at: pos.into(), 33 | cause: Box::new(err.into()) 34 | } 35 | } 36 | pub fn at(&self) -> &At { 37 | self.at.last() 38 | } 39 | pub fn cause(&self) -> ErrorKind { 40 | (*self.cause).clone() 41 | } 42 | } 43 | pub fn err, L: Into>>(err: E, pos: L) -> Error { 44 | Error::new(err,pos) 45 | } 46 | 47 | // An error falls into one of these categories, 48 | // or can be a context which itself contains an 49 | // error. 50 | #[derive(Clone,PartialEq,Debug)] 51 | pub enum ErrorKind { 52 | ApplicationError(ApplicationError), 53 | ImportError(ImportError), 54 | ShapeError(ShapeError), 55 | SyntaxError(SyntaxError), 56 | ContextError(ContextError) 57 | } 58 | 59 | impl ErrorText for ErrorKind { 60 | fn error_summary(&self) -> String { 61 | use self::ErrorKind::*; 62 | match *self { 63 | ApplicationError(ref e) => e.error_summary(), 64 | ImportError(ref e) => e.error_summary(), 65 | ShapeError(ref e) => e.error_summary(), 66 | SyntaxError(ref e) => e.error_summary(), 67 | ContextError(ref e) => e.error_summary() 68 | } 69 | } 70 | fn error_description(&self) -> String { 71 | use self::ErrorKind::*; 72 | match *self { 73 | ApplicationError(ref e) => e.error_description(), 74 | ImportError(ref e) => e.error_description(), 75 | ShapeError(ref e) => e.error_description(), 76 | SyntaxError(ref e) => e.error_description(), 77 | ContextError(ref e) => e.error_description() 78 | } 79 | } 80 | } 81 | 82 | impl Into> for ErrorKind { 83 | fn into(self: ErrorKind) -> Result { 84 | Err(self) 85 | } 86 | } 87 | impl Into> for ContextError { 88 | fn into(self: ContextError) -> Result { 89 | Err(self.into()) 90 | } 91 | } 92 | impl Into> for ApplicationError { 93 | fn into(self: ApplicationError) -> Result { 94 | Err(self.into()) 95 | } 96 | } 97 | impl Into> for ImportError { 98 | fn into(self: ImportError) -> Result { 99 | Err(self.into()) 100 | } 101 | } 102 | impl Into> for ShapeError { 103 | fn into(self: ShapeError) -> Result { 104 | Err(self.into()) 105 | } 106 | } 107 | impl Into> for SyntaxError { 108 | fn into(self: SyntaxError) -> Result { 109 | Err(self.into()) 110 | } 111 | } 112 | 113 | impl From for ErrorKind { 114 | fn from(err: ApplicationError) -> Self { 115 | ErrorKind::ApplicationError(err) 116 | } 117 | } 118 | impl From for ErrorKind { 119 | fn from(err: ImportError) -> Self { 120 | ErrorKind::ImportError(err) 121 | } 122 | } 123 | impl From for ErrorKind { 124 | fn from(err: ShapeError) -> Self { 125 | ErrorKind::ShapeError(err) 126 | } 127 | } 128 | impl From for ErrorKind { 129 | fn from(err: SyntaxError) -> Self { 130 | ErrorKind::SyntaxError(err) 131 | } 132 | } 133 | impl From for ErrorKind { 134 | fn from(err: ContextError) -> Self { 135 | ErrorKind::ContextError(err) 136 | } 137 | } 138 | 139 | // Error context - wrap other errors for positional information 140 | #[derive(Clone,PartialEq,Debug)] 141 | pub enum ContextError { 142 | At(Error) 143 | } 144 | 145 | impl ErrorText for ContextError { 146 | fn error_summary(&self) -> String { 147 | use self::ContextError::*; 148 | match *self { 149 | At{..} => { 150 | "From".to_owned() 151 | } 152 | } 153 | } 154 | fn error_description(&self) -> String { 155 | String::new() 156 | } 157 | } 158 | 159 | // Errors applying functions. 160 | #[derive(Clone,PartialEq,Debug)] 161 | pub enum ApplicationError { 162 | CantFindVariable(String,VarType), 163 | NotAFunction, 164 | WrongNumberOfArguments{expected: usize, got: usize}, 165 | WrongKindOfArguments{index: usize, expected: Vec, got: Kind}, 166 | WrongUnitOfArguments{index: usize, expected: Vec, got: String}, 167 | PropertyDoesNotExist(String), 168 | UnitMismatch, 169 | CycleDetected(Vec, String) 170 | } 171 | 172 | impl ErrorText for ApplicationError { 173 | fn error_summary(&self) -> String { 174 | use self::ApplicationError::*; 175 | match *self { 176 | CantFindVariable{..} => { 177 | "I cannot find this variable".to_owned() 178 | }, 179 | NotAFunction => { 180 | "This is not a function".to_owned() 181 | }, 182 | WrongNumberOfArguments{..} => { 183 | "The wrong number of arguments are being used here".to_owned() 184 | }, 185 | WrongKindOfArguments{..} => { 186 | "One or more of the arguments here are the wrong kind".to_owned() 187 | }, 188 | WrongUnitOfArguments{..} => { 189 | "One or more of the arguments here have the wrong unit".to_owned() 190 | }, 191 | PropertyDoesNotExist{..} => { 192 | "This property does not exist".to_owned() 193 | }, 194 | UnitMismatch => { 195 | "Units do not match".to_owned() 196 | }, 197 | CycleDetected{..} => { 198 | "A cycle has been detected".to_owned() 199 | } 200 | } 201 | } 202 | fn error_description(&self) -> String { 203 | use self::ApplicationError::*; 204 | match *self { 205 | CantFindVariable(ref name, ty) => { 206 | match ty { 207 | VarType::User => format!("The variable '{}' has not been declared", name), 208 | VarType::Builtin => format!("The built-in variable '{}' does not exist", name) 209 | } 210 | }, 211 | NotAFunction => { 212 | format!("Trying to use something here as a function, but it is not") 213 | }, 214 | WrongNumberOfArguments{expected,got} => { 215 | format!("This function expected {} arguments but got {}", expected, got) 216 | }, 217 | WrongKindOfArguments{index, ref expected, got} => { 218 | let e = expected.into_iter().map(|k| k.to_string()).collect::>().join(", "); 219 | format!("Argument {} is {}, but the function expected one of {}", index+1, got, e) 220 | }, 221 | WrongUnitOfArguments{index, ref expected, ref got} => { 222 | let e = expected.join(", "); 223 | format!("Argument {} is a unit with type '{}', but the function expected one of {}", index+1, got, e) 224 | }, 225 | PropertyDoesNotExist(ref prop) => { 226 | format!("trying to access the property '{}', which does not exist", prop) 227 | }, 228 | UnitMismatch => { 229 | format!("the suffixes of the units need to match but they do not") 230 | }, 231 | CycleDetected(ref vars, ref var) => { 232 | let cycle = vars 233 | .into_iter() 234 | .skip_while(|v| v != &var) 235 | .chain(iter::once(var)) 236 | .cloned() 237 | .collect::>() 238 | .join(" => "); 239 | format!("Variables were caught accessing each other in a cycle:\n {}", cycle) 240 | } 241 | } 242 | } 243 | } 244 | 245 | // Errors importing things. 246 | #[derive(Clone,PartialEq,Debug)] 247 | pub enum ImportError { 248 | CannotImportNoPathSet, 249 | CannotOpenFile(PathBuf), 250 | CannotReadFile(PathBuf), 251 | ImportLoop(Vec, PathBuf), 252 | CompileError(Error, PathBuf) 253 | } 254 | impl ErrorText for ImportError { 255 | fn error_summary(&self) -> String { 256 | use self::ImportError::*; 257 | match *self { 258 | CannotImportNoPathSet => { 259 | "I cannot import things".to_owned() 260 | }, 261 | CannotOpenFile{..} => { 262 | "I cannot open this file".to_owned() 263 | }, 264 | CannotReadFile{..} => { 265 | "I cannot read this file".to_owned() 266 | }, 267 | ImportLoop{..} => { 268 | "An import loop has been detected".to_owned() 269 | }, 270 | CompileError{..} => { 271 | "This file was imported from".to_owned() 272 | } 273 | } 274 | } 275 | fn error_description(&self) -> String { 276 | use self::ImportError::*; 277 | match *self { 278 | CannotImportNoPathSet => { 279 | "I am working from standard in, and so 'import' cannot be used as I don't know where to look for things".to_owned() 280 | }, 281 | CannotOpenFile{..} => { 282 | "Perhaps the path has been misspelt, or you do not have permission to access it?".to_owned() 283 | }, 284 | CannotReadFile{..} => { 285 | "The file exists, but you may not have permission to read it".to_owned() 286 | }, 287 | ImportLoop(ref paths, ref path) => { 288 | let cycle = paths 289 | .into_iter() 290 | .skip_while(|p| p != &path) 291 | .chain(iter::once(path)) 292 | .map(|p| p.display().to_string()) 293 | .collect::>() 294 | .join("\n "); 295 | format!("{}", cycle) 296 | }, 297 | CompileError{..} => { 298 | String::new() 299 | } 300 | } 301 | } 302 | } 303 | 304 | // Errors with the shaoe of the formed CSS. 305 | #[derive(Clone,PartialEq,Debug)] 306 | pub enum ShapeError { 307 | KeyframesKeyvalsNotAllowedAtTop, 308 | KeyframesKeyframesBlockNotAllowed, 309 | KeyframesFontFaceBlockNotAllowed, 310 | KeyframesMediaBlockNotAllowed, 311 | KeyframesNestedBlockNotAllowed, 312 | FontfaceBlockNotAllowed, 313 | InvalidExpressionInCssValue(Box), 314 | NakedKeyValNotAllowed, 315 | NotACSSBlock(Kind) 316 | } 317 | impl ErrorText for ShapeError { 318 | fn error_summary(&self) -> String { 319 | use self::ShapeError::*; 320 | match *self { 321 | KeyframesKeyvalsNotAllowedAtTop | 322 | KeyframesKeyframesBlockNotAllowed | 323 | KeyframesFontFaceBlockNotAllowed | 324 | KeyframesMediaBlockNotAllowed | 325 | KeyframesNestedBlockNotAllowed => { 326 | "There is a problem in this @keyframes block".to_owned() 327 | }, 328 | FontfaceBlockNotAllowed => { 329 | "There is a problem in this @font-face block".to_owned() 330 | }, 331 | InvalidExpressionInCssValue{..} => { 332 | "Invalid value being inserted into this CSS".to_owned() 333 | }, 334 | NakedKeyValNotAllowed => { 335 | "key:value pairs cannot be at the top level".to_owned() 336 | }, 337 | NotACSSBlock{..} => { 338 | "This should be a CSS block".to_owned() 339 | } 340 | } 341 | } 342 | fn error_description(&self) -> String { 343 | use self::ShapeError::*; 344 | match *self { 345 | KeyframesKeyvalsNotAllowedAtTop => { 346 | format!("key:value pairs aren't allowed directly inside a @keyframes block") 347 | }, 348 | KeyframesKeyframesBlockNotAllowed => { 349 | format!("@keyframes blocks aren't allowed inside other @keyframes blocks") 350 | }, 351 | KeyframesFontFaceBlockNotAllowed => { 352 | format!("@font-face blocks aren't allowed inside @keyframes blocks") 353 | }, 354 | KeyframesMediaBlockNotAllowed => { 355 | format!("@media blocks aren't allowed inside @keyframes blocks") 356 | }, 357 | KeyframesNestedBlockNotAllowed => { 358 | format!("nested blocks aren't allowed inside @keyframes block sections") 359 | }, 360 | FontfaceBlockNotAllowed => { 361 | format!("nested blocks aren't allowed inside @font-face blocks") 362 | }, 363 | InvalidExpressionInCssValue(ref expr) => { 364 | format!("I can't interpolate an expression that's {} into a CSS value. I can interpolate strings, booleans, units and colors into CSS values", expr.kind()) 365 | }, 366 | NakedKeyValNotAllowed => { 367 | format!("Key:value pairs need to be inside a block with a selector") 368 | }, 369 | NotACSSBlock(kind) => { 370 | format!("I expected a CSS block but got something that's {}", kind) 371 | } 372 | } 373 | } 374 | } 375 | 376 | // Errors parsing the syntax into an AST. 377 | #[derive(Clone,PartialEq,Debug)] 378 | pub enum SyntaxError { 379 | BadRule{ positives: Vec, negatives: Vec }, 380 | Custom{ message: String } 381 | } 382 | impl ErrorText for SyntaxError { 383 | fn error_summary(&self) -> String { 384 | "There was an error parsing this".to_owned() 385 | } 386 | fn error_description(&self) -> String { 387 | use self::SyntaxError::*; 388 | match *self { 389 | BadRule{ positives: ref _positives, negatives: ref _negatives } => { 390 | format!("Unexpected rule") 391 | } 392 | Custom{ ref message } => { 393 | format!("{}", message) 394 | } 395 | } 396 | } 397 | } 398 | 399 | // A trait allowing errors to carry a summary and description: 400 | pub trait ErrorText { 401 | fn error_summary(&self) -> String; 402 | fn error_description(&self) -> String; 403 | } -------------------------------------------------------------------------------- /src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | 3 | pub mod display; 4 | pub use self::errors::*; 5 | -------------------------------------------------------------------------------- /src/evaluator.rs: -------------------------------------------------------------------------------- 1 | use types::*; 2 | use errors::*; 3 | use std::collections::HashMap; 4 | use std::collections::HashSet; 5 | 6 | pub fn eval(e: &Expression, scope: Scope, context: &Context) -> Result { 7 | 8 | match e.expr { 9 | 10 | // String eg "hello" 11 | Expr::Str(ref s) => Ok(EvaluatedExpression::with_position(e.start, e.end, &context.path, EvaluatedExpr::Str(s.clone()))), 12 | 13 | // boolean eg true or false 14 | Expr::Bool(b) => Ok(EvaluatedExpression::with_position(e.start, e.end, &context.path, EvaluatedExpr::Bool(b))), 15 | 16 | // unit eg 12px, 100%, 30 17 | Expr::Unit(n, ref unit) => Ok(EvaluatedExpression::with_position(e.start, e.end, &context.path, EvaluatedExpr::Unit(n,unit.clone()))), 18 | 19 | // colour 20 | Expr::Colour(ref col) => Ok(EvaluatedExpression::with_position(e.start, e.end, &context.path, EvaluatedExpr::Colour(col.clone()))), 21 | 22 | // undefined 23 | Expr::Undefined => Ok(EvaluatedExpression::with_position(e.start, e.end, &context.path, EvaluatedExpr::Undefined)), 24 | 25 | // Variables: replace these with the Expresssion on scope that the 26 | // variable points to. Assume anything on scope is already simplified 27 | // as much as needed (this is important for Funcs, which use a scope 28 | // of vars to avoid replacing the func arg uses with other expressions) 29 | Expr::Var(ref name, ty) => { 30 | 31 | scope.find(name, ty).map_or( 32 | Err(err(ApplicationError::CantFindVariable(name.clone(), ty), At::position(&context.path, e.start, e.end))), 33 | |var| { Ok(var.and_position(e.start, e.end, &context.path)) } 34 | ) 35 | 36 | }, 37 | 38 | // If simplifies based on the boolean-ness of the condition! 39 | Expr::If{ cond: ref raw_cond, then: ref then_e, otherwise: ref else_e } => { 40 | 41 | let cond = eval(raw_cond, scope.clone(), context)?; 42 | 43 | use prelude::casting::raw_boolean; 44 | let is_true = match raw_boolean(cond.expr()){ 45 | Ok(b) => Ok(b), 46 | Err(e) => Err(err(e, At::position(&context.path, raw_cond.start, raw_cond.end))) 47 | }?; 48 | 49 | if is_true { 50 | eval(then_e, scope, context) 51 | } else { 52 | eval(else_e, scope, context) 53 | } 54 | }, 55 | 56 | // Func declarations: we want to store the scope that they are seen in 57 | // against the function so that it can be applied against the right things. 58 | // otherwise, leave as is. 59 | Expr::Func{ ref inputs, ref output } => { 60 | 61 | Ok(EvaluatedExpression::with_position( 62 | e.start, 63 | e.end, 64 | &context.path, 65 | EvaluatedExpr::Func{ 66 | inputs: inputs.clone(), 67 | output: output.clone(), 68 | scope: scope.clone(), 69 | context: context.clone() 70 | } 71 | )) 72 | 73 | }, 74 | 75 | // Access in the form of property access like $a.hello, or function application like $a(2,4) 76 | // access can be chained. 77 | Expr::Accessed{ ref expression, ref access } => { 78 | 79 | let mut curr: EvaluatedExpression = eval(expression, scope.clone(), context)?; 80 | 81 | for arg in access { 82 | match *arg { 83 | 84 | Accessor::Property{ ref name, ref location } => { 85 | 86 | if let EvaluatedExpr::Block(ref block) = *curr.clone().expr() { 87 | match block.scope.get(name) { 88 | Some(val) => { 89 | curr = val.and_position(location.start(), location.end(), &context.path); 90 | }, 91 | None => { 92 | curr = EvaluatedExpression::with_position(location.start(), location.end(), &context.path, EvaluatedExpr::Undefined); 93 | } 94 | } 95 | } else { 96 | return Err(err(ApplicationError::PropertyDoesNotExist(name.to_owned()), At::position(&context.path, location.start(), location.end()))); 97 | }; 98 | 99 | }, 100 | Accessor::Function{ ref args, ref location } => { 101 | 102 | let mut simplified_args = Vec::with_capacity(args.len()); 103 | for arg in args.into_iter() { 104 | let simplified_arg = eval(arg, scope.clone(), context)?; 105 | simplified_args.push(simplified_arg); 106 | } 107 | 108 | let function_e = curr.clone(); 109 | match *function_e.expr() { 110 | EvaluatedExpr::Func{ inputs: ref arg_names, output: ref func_e, scope: ref func_scope, context: ref func_context } => { 111 | 112 | // if too few args provided, set rest to undefined: 113 | while arg_names.len() > simplified_args.len() { 114 | simplified_args.push( EvaluatedExpression::with_position(location.start(), location.end(), &context.path, EvaluatedExpr::Undefined) ); 115 | } 116 | 117 | // complain if too many args are provided: 118 | if arg_names.len() != simplified_args.len() { 119 | return Err(err(ApplicationError::WrongNumberOfArguments{ 120 | expected: arg_names.len(), 121 | got: simplified_args.len() 122 | }, At::position(&context.path, e.start, e.end))); 123 | } 124 | 125 | // create scope containing simplified args to make use of in function body expr: 126 | let mut function_scope = HashMap::new(); 127 | for (name, arg) in arg_names.into_iter().zip(simplified_args) { 128 | function_scope.insert(name.to_owned(),arg); 129 | } 130 | 131 | // update our current expr to be the evaluated result. We evaluate the func_e expression 132 | // with respect to the scope and context it was seen against, and wrap any errors 133 | // with the location the function was called from (here). 134 | curr = eval(func_e, func_scope.push(function_scope), func_context).map_err(|e| { 135 | err(ContextError::At(e), At::location(&context.path, location.clone())) 136 | })?.and_position(location.start(), location.end(), &context.path); 137 | 138 | }, 139 | EvaluatedExpr::PrimFunc(ref func) => { 140 | 141 | // primitive func? just run it on the args then! 142 | curr = match func.0(&simplified_args, context) { 143 | Ok(res) => Ok(EvaluatedExpression::with_position(location.start(), location.end(), &context.path, res)), 144 | Err(error) => Err(err(error, At::location(&context.path, location.clone()))) 145 | }?; 146 | 147 | } 148 | _ => { 149 | return Err(err(ApplicationError::NotAFunction, At::position(&context.path, e.start, e.end))); 150 | } 151 | } 152 | } 153 | 154 | } 155 | } 156 | 157 | Ok(curr) 158 | 159 | }, 160 | 161 | // For Blocks, we do our best to simplify the block contents, complaining 162 | // if there is something invalid somewhere. 163 | Expr::Block(ref block) => { 164 | 165 | let block_scope = simplify_block_scope(&block.scope, &scope, context)?; 166 | let new_scope = scope.push(block_scope.clone()); 167 | 168 | let css = try_eval_cssentries(&block.css, &new_scope, context)?; 169 | let selector = try_cssbits_to_string(&block.selector, &new_scope, context)?; 170 | let mut ty = BlockType::Generic; 171 | 172 | // trim unnecessary spacing and newlines from within selector: 173 | let mut selector = selector 174 | .split(|c| c == ' ' || c == '\n') 175 | .filter(|s| s.len() > 0) 176 | .collect::>().join(" "); 177 | 178 | let media_str = "@media "; 179 | let keyframes_str = "@keyframes "; 180 | let fontface_str = "@font-face"; 181 | 182 | if selector.starts_with(media_str) { ty = BlockType::Media; selector = selector.replacen(media_str,"",1); } 183 | else if selector.starts_with(keyframes_str) { ty = BlockType::Keyframes; selector = selector.replacen(keyframes_str,"",1); } 184 | else if selector.starts_with(fontface_str) { ty = BlockType::FontFace; selector = selector.replacen(fontface_str,"",1); } 185 | 186 | Ok(EvaluatedExpression::with_position( 187 | e.start, 188 | e.end, 189 | &context.path, 190 | EvaluatedExpr::Block(EvaluatedBlock{ 191 | ty: ty, 192 | at: SmallVec::one(At::position(&context.path, e.start, e.end)), 193 | scope: block_scope, 194 | selector: selector, 195 | css: css 196 | }) 197 | )) 198 | 199 | }, 200 | 201 | 202 | } 203 | 204 | } 205 | 206 | // Scan through an expression, searching for variables provided in `search`, and adding any found 207 | // to `out`. Anything that introduces variables (function declaration and blocks) removes those from 208 | // search (since they shadow the names we actually care about finding). 209 | fn dependencies(e: &Expression, search: &HashSet) -> HashSet { 210 | 211 | fn get_dependencies_of(e: &Expression, search: &HashSet, out: &mut HashSet) { 212 | match e.expr { 213 | Expr::Str(..) => {}, 214 | Expr::Bool(..) => {}, 215 | Expr::Unit(..) => {}, 216 | Expr::Colour(..) => {}, 217 | Expr::Undefined => {}, 218 | Expr::If{ ref cond, ref then, ref otherwise } => { 219 | get_dependencies_of(cond, search, out); 220 | get_dependencies_of(then, search, out); 221 | get_dependencies_of(otherwise, search, out); 222 | }, 223 | Expr::Func{ ref inputs, ref output, .. } => { 224 | let mut search = search.clone(); 225 | for i in inputs { search.remove(i); } 226 | get_dependencies_of(output, &search, out); 227 | }, 228 | Expr::Var(ref name, ty) => { 229 | if ty != VarType::Builtin && search.contains(name) { 230 | out.insert(name.clone()); 231 | } 232 | }, 233 | Expr::Accessed{ ref expression, ref access } => { 234 | for accessor in access { 235 | if let Accessor::Function{ref args, ..} = *accessor { 236 | for arg in args { 237 | get_dependencies_of(arg, search, out); 238 | } 239 | } 240 | } 241 | get_dependencies_of(expression, search, out); 242 | }, 243 | Expr::Block(ref block) => { 244 | let search = get_dependencies_of_scope(&block.scope, &search.clone(), out); 245 | get_dependencies_of_cssbits(&block.selector, &search, out); 246 | get_dependencies_of_cssentries(&block.css, &search, out); 247 | } 248 | }; 249 | } 250 | fn get_dependencies_of_scope(scope: &HashMap, search: &HashSet, out: &mut HashSet) -> HashSet { 251 | let mut search = search.clone(); 252 | for k in scope.keys() { 253 | search.remove(k); 254 | } 255 | for e in scope.values() { 256 | get_dependencies_of(e, &search, out); 257 | } 258 | search 259 | } 260 | fn get_dependencies_of_cssbits(bits: &Vec, search: &HashSet, out: &mut HashSet) { 261 | for bit in bits { 262 | if let &CSSBit::Expr(ref e) = bit { 263 | get_dependencies_of(e, &search, out); 264 | } 265 | } 266 | } 267 | fn get_dependencies_of_cssentries(entries: &Vec, search: &HashSet, out: &mut HashSet) { 268 | for entry in entries { 269 | match *entry { 270 | CSSEntry::Expr(ref e) => { 271 | get_dependencies_of(e, search, out); 272 | }, 273 | CSSEntry::KeyVal{ ref key, ref val, .. } => { 274 | get_dependencies_of_cssbits(key, search, out); 275 | get_dependencies_of_cssbits(val, search, out); 276 | } 277 | } 278 | } 279 | } 280 | 281 | let mut out = HashSet::new(); 282 | get_dependencies_of(e, search, &mut out); 283 | out 284 | 285 | } 286 | 287 | // Given a map of dependencies (var name -> Expression + var deps), return a map of evaluated expressions, 288 | // or if a cycle is detected which prevents proper evaluation, an error. 289 | type Dependencies<'a> = HashMap)>; 290 | fn simplify_dependencies(deps: &Dependencies, scope: &Scope, context: &Context) -> Result,Error> { 291 | 292 | fn do_simplify(key: &String, last: &Vec, deps: &Dependencies, scope: &Scope, context: &Context, out: &mut HashMap) -> Result<(),Error> { 293 | 294 | if out.contains_key(key) { return Ok(()); } 295 | 296 | let &(expr,ref expr_deps) = deps.get(key).expect("Trying to simplify an expression but can't find it on scope"); 297 | 298 | if last.iter().any(|k| k == key) { 299 | return Err(err(ApplicationError::CycleDetected(last.clone(), key.clone()), At::position(&context.path,expr.start,expr.end))); 300 | } 301 | 302 | if expr_deps.len() > 0 { 303 | let mut new_last = last.clone(); 304 | new_last.push(key.clone()); 305 | for dep in expr_deps { 306 | do_simplify(dep, &new_last, deps, scope, context, out)?; 307 | } 308 | } 309 | 310 | let new_scope = scope.push(out.clone()); 311 | let new_expr = eval(expr, new_scope, context)?; 312 | out.insert(key.clone(), new_expr); 313 | Ok(()) 314 | 315 | }; 316 | 317 | let mut out = HashMap::new(); 318 | let last = Vec::new(); 319 | for (key,_) in deps { 320 | do_simplify(key, &last, deps, scope, context, &mut out)?; 321 | } 322 | Ok(out) 323 | 324 | } 325 | 326 | fn simplify_block_scope(block_scope: &HashMap, scope: &Scope, context: &Context) -> Result,Error> { 327 | 328 | // work out what each variable on scope depends on, so that we know which order to simplify them in in order 329 | // to ensure that each variable has in its scope a simplified version of everything it depends on. 330 | let vars: HashSet = block_scope.keys().cloned().collect(); 331 | let deps: Dependencies = block_scope.iter().map(|(k,v)| (k.clone(),(v,dependencies(v, &vars)))).collect(); 332 | 333 | simplify_dependencies(&deps, scope, context) 334 | } 335 | fn try_cssbits_to_string(bits: &Vec, scope: &Scope, context: &Context) -> Result { 336 | let mut string = vec![]; 337 | for bit in bits { 338 | match *bit { 339 | CSSBit::Str(ref s) => string.push(s.to_owned()), 340 | CSSBit::Expr(ref expr) => { 341 | let e = eval(expr, scope.clone(),context)?; 342 | use prelude::casting::raw_string; 343 | let s = match raw_string(e.expr()) { 344 | Ok(s) => Ok(s), 345 | Err(error) => Err(err(error, e.locations())) 346 | }?; 347 | string.push(s); 348 | } 349 | } 350 | } 351 | 352 | // trim unnecessary spacing and newlines from CSS value: 353 | let string = string 354 | .concat() 355 | .trim() 356 | .split(|c| c == ' ' || c == '\n') 357 | .filter(|s| s.len() > 0) 358 | .collect::>().join(" "); 359 | 360 | Ok(string) 361 | } 362 | fn try_eval_cssentries(entries: &Vec, scope: &Scope, context: &Context) -> Result,Error> { 363 | let mut out = vec![]; 364 | for val in entries { 365 | match *val { 366 | CSSEntry::Expr(ref expr) => { 367 | let css_expr = eval(expr, scope.clone(),context)?; 368 | match *css_expr.expr() { 369 | EvaluatedExpr::Block(ref block) => { 370 | 371 | // For any blocks nested in our block, update the block's 372 | // location to match that of the underlying expression, which 373 | // has done the work of capturing variable locations etc. 374 | let new_block = EvaluatedBlock { 375 | at: css_expr.locations().clone(), 376 | ..block.clone() 377 | }; 378 | out.push(EvaluatedCSSEntry::Block(new_block)); 379 | 380 | } 381 | ref e => return Err(err(ShapeError::NotACSSBlock(e.kind()), css_expr.locations())) 382 | }; 383 | }, 384 | CSSEntry::KeyVal{ref key, ref val, location} => { 385 | let key = try_cssbits_to_string(key, scope, context)?; 386 | let val = try_cssbits_to_string(val, scope, context)?; 387 | out.push(EvaluatedCSSEntry::KeyVal{ 388 | key: key, 389 | val: val, 390 | at: At::location(&context.path, location) 391 | }); 392 | } 393 | } 394 | } 395 | Ok(out) 396 | } 397 | 398 | 399 | #[cfg(test)] 400 | pub mod tests { 401 | 402 | // use super::*; 403 | 404 | } -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #![macro_use] 2 | 3 | /// make it a little easier to build up a scope of primitive functions 4 | macro_rules! scope { 5 | ( $($key:expr => $item:expr);+ ) => ({ 6 | 7 | use std::collections::HashMap; 8 | use types::Scope; 9 | 10 | let mut map = HashMap::new(); 11 | $( 12 | map.insert($key.to_owned(), 13 | EvaluatedExpression::new($item) 14 | ); 15 | )* 16 | Scope::from(map) 17 | 18 | }) 19 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | // #[macro_use] extern crate chomp; 4 | //#[macro_use] extern crate lazy_static; 5 | extern crate pest; 6 | #[macro_use] extern crate pest_derive; 7 | #[macro_use] extern crate clap; 8 | 9 | mod macros; 10 | mod parser; 11 | mod types; 12 | mod evaluator; 13 | mod cache; 14 | mod outputter; 15 | mod prelude; 16 | mod errors; 17 | 18 | use clap::App; 19 | // this imports and evaluates a FUSS file: 20 | use prelude::import::{import_root,import_string}; 21 | use std::convert::From; 22 | use std::path::PathBuf; 23 | use types::*; 24 | use errors::display; 25 | use std::thread; 26 | 27 | use std::io::{self, Read}; 28 | 29 | fn main() { 30 | run(); 31 | } 32 | 33 | fn run() { 34 | 35 | let child = thread::Builder::new().stack_size(64 * 1024 * 1024).spawn(move || { 36 | 37 | // parse args from CLI based on our cli.yml, 38 | // and get matched commands: 39 | let yaml = load_yaml!("cli.yml"); 40 | let matches = App::from_yaml(yaml).get_matches(); 41 | let maybe_path = matches.value_of("input").map(PathBuf::from); 42 | 43 | let input_string = if maybe_path.is_none() { 44 | let mut buffer = String::new(); 45 | let stdin = io::stdin(); 46 | let mut handle = stdin.lock(); 47 | handle.read_to_string(&mut buffer).unwrap(); 48 | buffer 49 | } else { 50 | String::new() 51 | }; 52 | 53 | // import the FUSS file, compiling and evaluating it. 54 | // if path provided, use that, else pull from stdin. 55 | let res = match maybe_path { 56 | Some(path) => { 57 | import_root(&path) 58 | }, 59 | None => { 60 | import_string(&input_string) 61 | } 62 | }; 63 | 64 | match res { 65 | Err(e) => { 66 | display::display_import_error(e, display::Options::with_stdin(&input_string)); 67 | }, 68 | Ok(EvaluatedExpr::Block(block)) => { 69 | let warnings = outputter::print_css(block); 70 | let opts = display::Options::with_stdin(&input_string); 71 | for warning in warnings { 72 | display::display_warning(warning, &opts); 73 | } 74 | }, 75 | Ok(e) => { 76 | eprintln!("Fuss file needs to evaluate to a css block, but instead evaluates to: {}", e.kind()); 77 | } 78 | } 79 | 80 | }).unwrap(); 81 | 82 | child.join().unwrap() 83 | 84 | } 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/outputter.rs: -------------------------------------------------------------------------------- 1 | use types::*; 2 | use errors::*; 3 | use std::io::{self, Write}; 4 | 5 | /// the only thing we expose from this: 6 | pub fn print_css(block: EvaluatedBlock) -> Vec { 7 | 8 | let mut items = Items::new(); 9 | items.populate_from_block(block); 10 | 11 | let mut s = String::new(); 12 | let mut warnings = vec![]; 13 | 14 | // print global fontfaces: 15 | for font in items.fontfaces { 16 | match font { 17 | Ok(block) => { 18 | fontface_into_string(0, block, &mut s); 19 | }, 20 | Err(e) => { 21 | warnings.push(e); 22 | } 23 | }; 24 | } 25 | 26 | // print global keyframe animations: 27 | for keyframe in items.keyframes { 28 | match keyframe { 29 | Ok(block) => { 30 | keyframes_into_string(0, block, &mut s); 31 | }, 32 | Err(e) => { 33 | warnings.push(e); 34 | } 35 | } 36 | } 37 | 38 | // print CSS blocks: 39 | for m in items.media { 40 | 41 | let is_media = m.media.len() > 0; 42 | let indent = if is_media { 1 } else { 0 }; 43 | 44 | if is_media { 45 | s += "@media "; 46 | s += &merge_media_query(m.media); 47 | s += " {\n"; 48 | } 49 | 50 | if m.fontfaces.len() > 0 { 51 | for font in m.fontfaces { 52 | match font { 53 | Ok(block) => { 54 | fontface_into_string(indent, block, &mut s); 55 | }, 56 | Err(e) => { 57 | warnings.push(e); 58 | } 59 | }; 60 | } 61 | } 62 | 63 | if m.keyframes.len() > 0 { 64 | for keyframe in m.keyframes { 65 | match keyframe { 66 | Ok(block) => { 67 | keyframes_into_string(indent, block, &mut s); 68 | }, 69 | Err(e) => { 70 | warnings.push(e); 71 | } 72 | } 73 | } 74 | } 75 | 76 | for style in m.styles { 77 | if style.selector.len() > 0 { 78 | css_block_into_string(indent, merge_css_selector(style.selector), style.keyvals, &mut s); 79 | } else if style.keyvals.len() > 0 { 80 | for KeyVals{at,..} in style.keyvals { 81 | warnings.push(err(ShapeError::NakedKeyValNotAllowed, at)) 82 | } 83 | } 84 | } 85 | 86 | if is_media { 87 | s += "}\n" 88 | } 89 | 90 | } 91 | 92 | let stdout = io::stdout(); 93 | let mut handle = stdout.lock(); 94 | handle.write_all(s.as_bytes()).expect("failed to write to stdout"); 95 | warnings 96 | } 97 | 98 | fn keyframes_into_string(indent_count: usize, block: Keyframes, s: &mut String) { 99 | let indent: String = (0..indent_count).map(|_| '\t').collect(); 100 | 101 | *s += &indent; 102 | *s += "@keyframes "; 103 | *s += &block.name; 104 | *s += " {\n"; 105 | for section in block.inner { 106 | css_block_into_string(indent_count+1, section.selector, section.keyvals, s); 107 | } 108 | *s += &indent; 109 | *s += "}\n"; 110 | } 111 | 112 | fn fontface_into_string(indent_count: usize, block: FontFace, s: &mut String) { 113 | let indent: String = (0..indent_count).map(|_| '\t').collect(); 114 | 115 | *s += &indent; 116 | *s += "@font-face {\n"; 117 | css_keyvals_into_string(indent_count+1, block.keyvals, s); 118 | *s += &indent; 119 | *s += "}\n"; 120 | } 121 | 122 | fn css_block_into_string(indent_count: usize, selector: String, css: Vec, s: &mut String) { 123 | let indent: String = (0..indent_count).map(|_| '\t').collect(); 124 | 125 | *s += &indent; 126 | *s += selector.trim(); 127 | *s += " {\n"; 128 | css_keyvals_into_string(indent_count+1, css, s); 129 | *s += &indent; 130 | *s += "}\n"; 131 | } 132 | 133 | fn css_keyvals_into_string(indent_count: usize, css: Vec, s: &mut String) { 134 | let indent: String = (0..indent_count).map(|_| '\t').collect(); 135 | 136 | for KeyVals{keyvals,..} in css { 137 | for KeyVal{key,val} in keyvals { 138 | *s += &indent; 139 | *s += key.trim(); 140 | *s += ": "; 141 | *s += val.trim(); 142 | *s += ";\n"; 143 | } 144 | } 145 | } 146 | 147 | fn merge_media_query(query: Vec) -> String { 148 | query.join(" and ") 149 | } 150 | fn merge_css_selector(mut selector: Vec) -> String { 151 | 152 | selector.reverse(); 153 | 154 | let mut current: Vec = match selector.pop() { 155 | Some(val) => val.split(',').map(|s| s.trim().to_owned()).collect(), 156 | None => return String::new() 157 | }; 158 | 159 | while let Some(next) = selector.pop() { 160 | 161 | let mut new_current = vec![]; 162 | 163 | // if we have multiple selectors separated by ",", 164 | // apply each one independently to each selector we have so far. 165 | for next in next.split(',').map(|s| s.trim()) { 166 | 167 | for mut curr in current.iter().cloned() { 168 | 169 | // replace and '&'s in a selector with the previous, if any exist. 170 | // else, just append selectors separated by a space. 171 | if next.contains('&') { 172 | curr = next.replace('&', &curr) 173 | } else { 174 | curr.push(' '); 175 | curr.push_str(next); 176 | } 177 | new_current.push(curr); 178 | 179 | } 180 | 181 | } 182 | current = new_current; 183 | 184 | } 185 | 186 | current.join(", ") 187 | } 188 | 189 | #[derive(Clone,PartialEq,Debug)] 190 | struct Loc { 191 | media: Vec, 192 | selector: Vec, 193 | at: SmallVec 194 | } 195 | 196 | #[derive(Clone,PartialEq,Debug)] 197 | struct Media { 198 | media: Vec, 199 | fontfaces: Vec>, 200 | styles: Vec