From d82ad0ae29a9958838c1734763a4b29fc95d514c Mon Sep 17 00:00:00 2001 From: Alex Palaistras Date: Thu, 19 May 2016 21:45:17 +0100 Subject: [PATCH] Expand mixin support, fix README docs --- README.md | 45 ++++++++++--- fawkss | 148 +++++++++++++++++++++++++++++-------------- tests/04-mixins.scss | 48 ++++++++++++++ 3 files changed, 185 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 68c44a2..cae39dc 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,42 @@ This is more of a fun project than a production-ready piece of software, though ## Status -Currently, the following features are implemented: +Currently, the following SASS features are implemented, in varying degrees: - * C99/C++-style comments (i.e. `// This is a comment`) - * Variables - * Imports +### C99/C++-style comments -A full test-suite is provided (depending only on `make` and `awk`), which should serve as a good example of the existing feature-set. +Lines containing comments using the `//` syntax allow for including commentary that does not appear in the processed CSS output. Support for the common `/* */` syntax remains the same, and affected lines are not removed from the final output. + +### Variables + +Variables can be declared and used with the following syntax: + +```scss +$variable-name: 10px; +width: $variable-name; +``` + +All variables are defined in the global scope, regardless of where they've been defined. Variable names can contain alphanumeric characters, as well as underscore and hyphen characters, and are always prefixed with a dollar sign. + +Only one variable declaration can appear on a single line, though once defined, a variable can be used as many times as needed, including on the same line. Redeclaring a variable overrides its previous value. + + +### Imports + +CSS import declarations are extended for including files transparently in the same output file. Imports reference files with the `.scss` extension, but will resolve to such files even if not specified in the import declaration. Thus, for a file of name `common/_colors.scss`, the following declarations are equivalent: + +```scss +@import "common/colors" +@import "common/colors.scss" +@import "common/_colors" +@import "common_colors.scss" +``` + +Files with leading underscores are handled as partials with additional restrictions by SASS, though Fawkss makes no such distinctions. + +### Mixins + +Currently supported are mixins with optional parameters (including default values) and nested mixins. Features such as variadic parameters and mixins with parent selectors are forthcoming. ## Roadmap @@ -24,13 +53,11 @@ That being said, I do not plan to implement any context-sensitive functionality A `Makefile` is provided for running tests and producing documentation for Fawkss. Run `make help` in the project root for more information. -## Are you kidding me? - -Nope. I wouldn't suggest you actually use this for anything, though. +A full test-suite is provided (depending only on `make` and `awk`), which should serve as a good example of the existing feature-set. ## License All code in this repository is covered by the terms of the MIT License, the full text of which can be found in the LICENSE file. [license-url]: https://github.com/deuill/go-php/blob/master/LICENSE -[license-svg]: https://img.shields.io/badge/license-MIT-blue.svg \ No newline at end of file +[license-svg]: https://img.shields.io/badge/license-MIT-blue.svg diff --git a/fawkss b/fawkss index 35be173..d70942b 100755 --- a/fawkss +++ b/fawkss @@ -63,6 +63,23 @@ function file_exists(filename) { return (system("[ -e '" filename "' ]") == 0) ? 1 : 0; } +# > Function 'read_line' gets the next line from import, as specified by `type`, +# > pointed to by `name`. Two types are currently specified, "file" and "cmd". +# > +# > Type "file" will attempt to get a newline-delimited line of a file pointed +# > to by `name`, and type "cmd" will get a newline-delimited line from a shell +# > command string. +function read_line(name, type) { + switch (type) { + case "file": + return (getline < name) + case "cmd": + return (name | getline) + } + + return 0 +} + # Global declarations # ------------------- # @@ -78,6 +95,8 @@ BEGIN { errors["mixin-exists"] = "ERROR: Mixin '%s' already defined, line %d\n" errors["mixin-undefined"] = "ERROR: Use of undefined mixin '%s', line %d\n" + errors["mixin-param-undefined"] = "ERROR: Parameter '%s' undefined for mixin '%s', line %d\n" + # Rule definitions. rules["comment-define"] = "[ ]*//.*$" rules["comment-exception"] = "['\"][^//]*//[^'\"]*['\"]" @@ -91,16 +110,16 @@ BEGIN { rules["mixin-name"] = "[a-zA-Z0-9_-]+" rules["mixin-param"] = rules["variable-name"] "([ ]*:[ ]*[^,;]+)?" - rules["mixin-attr"] = "(" rules["variable-name"] "[ ]*:[ ]*)?[^,;]+" + rules["mixin-arg"] = "(" rules["variable-name"] "[ ]*:[ ]*)?[^,;]+" rules["mixin-params"] = "\\([ ]*" rules["mixin-param"] "([ ]*,[ ]*" rules["mixin-param"] ")*[ ]*\\)" - rules["mixin-attrs"] = "\\([ ]*" rules["mixin-attr"] "([ ]*,[ ]*" rules["mixin-attr"] ")*[ ]*\\)" + rules["mixin-args"] = "\\([ ]*" rules["mixin-arg"] "([ ]*,[ ]*" rules["mixin-arg"] ")*[ ]*\\)" rules["mixin-define"] = "^[\t ]*@mixin[ ]+" rules["mixin-name"] "(" rules["mixin-params"] ")?" - rules["mixin-include"] = "^[\t ]*@include[ ]+" rules["mixin-name"] "(" rules["mixin-attrs"] ")?[ ]*;" + rules["mixin-include"] = "^[\t ]*@include[ ]+" rules["mixin-name"] "(" rules["mixin-args"] ")?[ ]*;" } -# Import stack initialization +# Import stack initialisation # ---------------------------- # # This block initializes the import stack with the current filename, and reads @@ -110,13 +129,13 @@ BEGIN { { # File import stack. - imports["cmd"] = "" imports["length"] = 0 - imports[imports["length"]] = FILENAME + imports[imports["length"] ":name"] = FILENAME + imports[imports["length"] ":type"] = "file" # Read from import file stack line-by-line until stack is exhausted. while (imports["length"] >= 0) { - while ((imports["cmd"] != "" && (imports["cmd"] | getline) > 0) || (getline < imports[imports["length"]]) > 0) { + while (read_line(imports[imports["length"] ":name"], imports[imports["length"] ":type"]) > 0) { # Rule definitions # ---------------- @@ -140,7 +159,7 @@ if ($0 ~ rules["import-define"]) { # Extract file and directory name parts from path, and append root directory # path for current file to directory part. - dir = dirname(imports[imports["length"]]) dirname(path) + dir = dirname(imports[imports["length"] ":name"]) dirname(path) file = basename(path) # Check filename against all potential filename variations. @@ -157,22 +176,26 @@ if ($0 ~ rules["import-define"]) { # Return error if import file was not found. if (!exists) { - printf errors["import-not-found"], path, imports[imports["length"]], FNR | "cat >&2" + printf errors["import-not-found"], path, imports[imports["length"] ":name"], FNR | "cat >&2" exit 1 } # Check for cyclic imports. if (filename in processed) { - printf errors["import-cyclic"], filename, imports[imports["length"]], FNR | "cat >&2" + printf errors["import-cyclic"], filename, imports[imports["length"] ":name"], FNR | "cat >&2" exit 1 } - # Add filename to list of processed files. + # Add filename to list of processed imports. processed[filename] = 1 - # Push filename to stack of processed files and continue to next line. + # Push filename to stack of imports. imports["length"] += 1 - imports[imports["length"]] = filename + imports[imports["length"] ":name"] = filename + + # Specify import type as file. + imports[imports["length"] ":type"] = "file" + continue } @@ -195,15 +218,15 @@ if ($0 ~ rules["mixin-define"]) { } # Store mixin parameters, if any. - match($0, rules["mixin-param-list"]) - if (RSTART > 0) { + match($0, rules["mixin-params"]) + if (RLENGTH > 0) { mixins[name ":params"] = substr($0, RSTART + 1, RLENGTH - 2) } # Read mixin contents until we encounter a closing bracket. The bracket must # appear on its own line, otherwise the final member of the mixin will not # be parsed. - while ((getline line < imports[imports["length"]]) > 0 && line !~ "}" ) { + while ((getline line < imports[imports["length"] ":name"]) > 0 && line !~ "}" ) { mixins[name] = mixins[name] "\n" line } @@ -232,6 +255,64 @@ if ($0 ~ rules["variable-define"]) { continue } +# > Match mixin includes, for example: +# > +# > body { +# > @include big-font; +# > } +# > +# > Attempting to use an undefined mixin will throw a fatal error. +if ($0 ~ rules["mixin-include"]) { + # Get mixin name. + match($2, rules["mixin-name"]) + name = substr($2, RSTART, RLENGTH) + + # Check for invalid mixin name. + if (!name in mixins) { + printf errors["mixin-undefined"], name, FNR | "cat >&2" + exit 1 + } + + # Copy mixin contents for further processing. + contents = mixins[name] + + # Check if mixin defines parameters, and attempt to use arguments passed. + if (name ":params" in mixins) { + # Split parameters as individual tokens. + split(mixins[name ":params"], params, "[ ]*,[ ]*") + + # Get arguments passed to include declaration. + match($0, rules["mixin-args"]) + split(substr($0, RSTART + 1, RLENGTH - 2), args, "[ ]*,[ ]*") + + # Check arguments against parameters, substituting each with the other, + # using default values where available when an argument hasn't been passed. + for (i in params) { + # Attempt to split parameter in name and value parts. + split(params[i], p, "[ ]*:[ ]*") + + # Throw error if parameter has no default value and no corresponding + # argument has been passed. + if (length(p) == 1 && args[i] == "") { + printf errors["mixin-param-undefined"], p[1], name, FNR | "cat >&2" + exit 1 + } + + # Replace parameters with the concrete argument values. + gsub("\\" p[1], (args[i] == "") ? p[2] : args[i], contents) + } + } + + # Push mixin to import stack. + imports["length"] += 1 + imports[imports["length"] ":name"] = "echo '" contents "'" + + # Specify import type as command. + imports[imports["length"] ":type"] = "cmd" + + continue +} + # > Match inline comments, for example: # > # > // This is an inline comment. @@ -269,28 +350,6 @@ if ($0 ~ rules["comment-define"]) { } } -# > Match mixin imports, for example: -# > -# > body { -# > @include big-font; -# > } -# > -# > Attempting to use an undefined mixin will throw a fatal error. -if ($0 ~ rules["mixin-include"]) { - # Get mixin name. - match($2, rules["mixin-name"]) - name = substr($2, RSTART, RLENGTH) - - # Check for invalid mixin name. - if (!name in mixins) { - printf errors["mixin-undefined"], name, FNR | "cat >&2" - exit 1 - } - - imports["cmd"] = "echo '" mixins[name] "'" - continue -} - # > Match variable uses, for example: # > # > :root{background: $white;} @@ -304,7 +363,7 @@ if ($0 ~ rules["variable-name"]) { # Throw error and exit if variable used has not been declared. if (variables[name] == "") { - printf errors["variable-undefined"], name, imports[imports["length"]], FNR | "cat >&2" + printf errors["variable-undefined"], name, imports[imports["length"] ":name"], FNR | "cat >&2" exit 1 } @@ -344,15 +403,10 @@ print # reference popped from the top of the stack. If the stack is left empty, the # program continues to cleanup and exit, as defined in the block below. -if (imports["cmd"] != "") { - close(imports["cmd"]) - imports["cmd"] = "" -} else { - close(imports[imports["length"]]) +close(imports[imports["length"] ":name"]) +delete processed[imports[imports["length"] ":name"]] - delete processed[imports[imports["length"]]] - imports["length"] -= 1 -} +imports["length"] -= 1 } diff --git a/tests/04-mixins.scss b/tests/04-mixins.scss index ad29b22..ec61bfe 100644 --- a/tests/04-mixins.scss +++ b/tests/04-mixins.scss @@ -14,6 +14,39 @@ body { @include invisible-ink; } +// Nested mixin with additional rules. +@mixin invisible-box { + @include invisible-ink; + border: none; +} + +#boxy-mcboxface { + @include invisible-box; +} + +// Simple mixin with parameters. +@mixin paint-it($fg-color, $bg-color) { + color: $fg-color; + background-color: $bg-color; +} + +#black-box { + @include paint-it(black, black); +} + +// Mixin with default parameters and overrides. +$color-1: black; +$color-2: white; +$color-3: red; + +@mixin gradient-me($color-1: purple, $color-2: black) { + linear-gradient(left, $color-1, $color-2, $color-3); +} + +#rainbox-box { + @include gradient-me(green); +} + --- EXPECTED --- body { @@ -21,4 +54,19 @@ body { background-color: white; } +#boxy-mcboxface { + color: white; + background-color: white; + border: none; +} + +#black-box { + color: black; + background-color: black; +} + +#rainbox-box { + linear-gradient(left, green, black, red); +} + --- END --- \ No newline at end of file