mirror of https://github.com/deuill/fawkss.git
421 lines
13 KiB
Awk
Executable File
421 lines
13 KiB
Awk
Executable File
#!/usr/bin/awk -f
|
|
#
|
|
# Fawkss — The [ig]noble CSS preprocessor.
|
|
# ========================================
|
|
#
|
|
# Fawkss is a CSS preprocessor for people who dislike CSS preprocessors. It
|
|
# implements a subset of the SASS syntax while remaining relatively simple.
|
|
#
|
|
# This documentation is built using Markdown syntax, and can be parsed out by
|
|
# running `make doc` in the project root. Please check the project's README file
|
|
# for additional information.
|
|
#
|
|
# Built-in functions
|
|
# ------------------
|
|
#
|
|
# This section contains global helper functions, used across different rules, as
|
|
# defined in the next section below.
|
|
#
|
|
# > Function `firstindex` returns the index of the first occurence of a regular
|
|
# > expression `r` in `str`, or 0 if the none was found.
|
|
function firstindex(str, r) {
|
|
match(str, r)
|
|
return max(RSTART, 0)
|
|
}
|
|
|
|
# > Function `lastindex` returns the index of the last occurence of a regular
|
|
# > expression `r` in `str`, or 0 if the none was found.
|
|
function lastindex(str, r) {
|
|
match(str, ".*" r)
|
|
return max(RSTART + RLENGTH - 1, 0)
|
|
}
|
|
|
|
# > Function `max` finds and returns greatest between two numbers.
|
|
function max(x, y) {
|
|
return (x > y) ? x : y
|
|
}
|
|
|
|
# > Function `trim` removes characters (whitespace by default) off both ends of
|
|
# > the string passed and returns the modified string.
|
|
function trim(str, ch) {
|
|
ch = (ch != "") ? ch : " "
|
|
str = substr(str, firstindex(str, "[^" ch "]"))
|
|
return substr(str, 0, lastindex(str, "[^" ch "]"))
|
|
}
|
|
|
|
# > Function `basename` strips the directory name from file paths and returns
|
|
# > the resulting file name.
|
|
function basename(path) {
|
|
match(path, "[^/]+$")
|
|
return substr(path, RSTART, RLENGTH)
|
|
}
|
|
|
|
# > Function `dirname` strips the last component from a path and returns the
|
|
# > resulting path.
|
|
function dirname(path) {
|
|
match(path, "^.+/")
|
|
return substr(path, RSTART, RLENGTH)
|
|
}
|
|
|
|
# > Function `file_exists` checks if file pointed to by `filename` exists, and
|
|
# > returns `1` if true, `0` if false.
|
|
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
|
|
# -------------------
|
|
#
|
|
# This block contains logic for initializing global variables used across Fawkss.
|
|
|
|
BEGIN {
|
|
# Error messages used across Fawkss.
|
|
errors["variable-undefined"] = "ERROR: Use of undefined variable '%s' in file '%s', line %d\n"
|
|
|
|
errors["import-cyclic"] = "ERROR: Cyclic import of file '%s' in file '%s', line %d\n"
|
|
errors["import-not-found"] = "ERROR: Import file '%s' not found, defined in '%s', line %d\n"
|
|
|
|
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"] = "['\"][^//]*//[^'\"]*['\"]"
|
|
|
|
rules["variable-name"] = "\\$[a-zA-Z0-9_-]+"
|
|
rules["variable-define"] = "^[\t ]*" rules["variable-name"] "[ ]*:"
|
|
|
|
rules["import-path"] = "['\"][^'\".]+(.scss)?[ ]*['\"]"
|
|
rules["import-define"] = "^[\t ]*@import[ ]+" rules["import-path"] "[ ]*;"
|
|
rules["import-variants"] = "%s%s.scss,%s_%s.scss,%s%s,%s_%s"
|
|
|
|
rules["mixin-name"] = "[a-zA-Z0-9_-]+"
|
|
rules["mixin-param"] = rules["variable-name"] "([ ]*:[ ]*[^,;]+)?"
|
|
rules["mixin-arg"] = "(" rules["variable-name"] "[ ]*:[ ]*)?[^,;]+"
|
|
|
|
rules["mixin-params"] = "\\([ ]*" rules["mixin-param"] "([ ]*,[ ]*" rules["mixin-param"] ")*[ ]*\\)"
|
|
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-args"] ")?[ ]*;"
|
|
}
|
|
|
|
# Import stack initialisation
|
|
# ----------------------------
|
|
#
|
|
# This block initializes the import stack with the current filename, and reads
|
|
# from the top line-by-line until the stack is exhausted. Import declarations
|
|
# switch the read context by pushing to the stack, and are popped when the read
|
|
# operation reaches EOF or any error.
|
|
|
|
{
|
|
# File import stack.
|
|
imports["length"] = 0
|
|
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 (read_line(imports[imports["length"] ":name"], imports[imports["length"] ":type"]) > 0) {
|
|
|
|
# Rule definitions
|
|
# ----------------
|
|
#
|
|
# This block contains definitions for line manipulation rules used across Fawkss.
|
|
# Rules may or may not be exclusive, i.e. the effects of one rule may cascade to
|
|
# subsequent rules for the same line.
|
|
#
|
|
# > Match import declarations, for example:
|
|
# >
|
|
# > @import "partials/colors";
|
|
# >
|
|
# > The above declaration may match file `_colors.scss` or `colors.scss` in the
|
|
# > `partials` directory (which should exist on the same level as the calling
|
|
# > file). Imports can be nested (i.e. imported files may in turn import other
|
|
# > files), and cyclic dependancies will return a fatal error.
|
|
if ($0 ~ rules["import-define"]) {
|
|
# Extract path part from import declaration.
|
|
match($0, rules["import-path"])
|
|
path = substr($0, RSTART + 1, RLENGTH - 2)
|
|
|
|
# Extract file and directory name parts from path, and append root directory
|
|
# path for current file to directory part.
|
|
dir = dirname(imports[imports["length"] ":name"]) dirname(path)
|
|
file = basename(path)
|
|
|
|
# Check filename against all potential filename variations.
|
|
exists = 0
|
|
split(rules["import-variants"], variants, ",")
|
|
|
|
for (v in variants) {
|
|
filename = sprintf(variants[v], dir, file)
|
|
if (file_exists(filename)) {
|
|
exists = 1
|
|
break
|
|
}
|
|
}
|
|
|
|
# Return error if import file was not found.
|
|
if (!exists) {
|
|
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"] ":name"], FNR | "cat >&2"
|
|
exit 1
|
|
}
|
|
|
|
# Add filename to list of processed imports.
|
|
processed[filename] = 1
|
|
|
|
# Push filename to stack of imports.
|
|
imports["length"] += 1
|
|
imports[imports["length"] ":name"] = filename
|
|
|
|
# Specify import type as file.
|
|
imports[imports["length"] ":type"] = "file"
|
|
|
|
continue
|
|
}
|
|
|
|
# > Match mixin declarations, for example:
|
|
# >
|
|
# > @mixin big-font {
|
|
# > font-size: 200%;
|
|
# > }
|
|
# >
|
|
# > Mixin declarations can then be used using `@include`, defined below.
|
|
if ($0 ~ rules["mixin-define"]) {
|
|
# Get mixin name.
|
|
match($2, rules["mixin-name"])
|
|
name = substr($2, RSTART, RLENGTH)
|
|
|
|
# Check for unique mixin name.
|
|
if (name in mixins) {
|
|
printf errors["mixin-exists"], name, FNR | "cat >&2"
|
|
exit 1
|
|
}
|
|
|
|
# Store mixin parameters, if any.
|
|
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"] ":name"]) > 0 && line !~ "}" ) {
|
|
mixins[name] = mixins[name] "\n" line
|
|
}
|
|
|
|
# Remove leading newline from mixin.
|
|
mixins[name] = substr(mixins[name], 2)
|
|
|
|
continue
|
|
}
|
|
|
|
# > Match variable declarations, for example:
|
|
# >
|
|
# > $varname: "value";
|
|
# >
|
|
# > Only one variable declaration can appear on a single line. Redeclaring a
|
|
# > variable overrides the value set for that variable.
|
|
if ($0 ~ rules["variable-define"]) {
|
|
# Split text in tokens.
|
|
split($0, token, ":")
|
|
|
|
# Get variable name and value.
|
|
name = trim(substr(token[1], index(token[1], "$")))
|
|
value = trim(substr(token[2], 0, lastindex(token[2], ";") - 1))
|
|
|
|
# Assign variable to the global variables table.
|
|
variables[name] = value
|
|
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.
|
|
# > :root{background: white;} // Another inline comment.
|
|
# >
|
|
# > As opposed to regular CSS comments (i.e. `/* */`), inline comments are removed
|
|
# > from the processed result. Inline comments inside strings are not removed.
|
|
if ($0 ~ rules["comment-define"]) {
|
|
# Initialize local variables.
|
|
len = 0
|
|
|
|
# Remove any special cases from the line.
|
|
while (match($0, rules["comment-exception"])) {
|
|
special[len += 1] = RSTART ":" substr($0, RSTART, RLENGTH)
|
|
$0 = substr($0, 0, RSTART - 1) substr($0, RSTART + RLENGTH, length($0))
|
|
}
|
|
|
|
# Remove inline comments from line.
|
|
while (match($0, rules["comment-define"])) {
|
|
$0 = substr($0, 0, RSTART - 1) substr($0, RSTART + RLENGTH, length($0))
|
|
}
|
|
|
|
# Reinsert special cases in their predefined positions.
|
|
for (i = len; i != 0; i--) {
|
|
pos = substr(special[i], 0, index(special[i], ":") - 1)
|
|
|
|
# Do not attempt to reinsert special case string if string has been
|
|
# truncated to less the original position of the string.
|
|
if (pos > length($0)) {
|
|
continue
|
|
}
|
|
|
|
str = substr(special[i], index(special[i], ":") + 1, length(special[i]))
|
|
$0 = substr($0, 0, pos - 1) str substr($0, pos, length($0))
|
|
}
|
|
}
|
|
|
|
# > Match variable uses, for example:
|
|
# >
|
|
# > :root{background: $white;}
|
|
# >
|
|
# > Where `$white` is a previously declared variable. Attempting to use a variable
|
|
# > that has not been defined yet will throw a fatal error.
|
|
if ($0 ~ rules["variable-name"]) {
|
|
# Replace each variable use with its concrete value.
|
|
while (match($0, rules["variable-name"])) {
|
|
name = substr($0, RSTART, RLENGTH)
|
|
|
|
# Throw error and exit if variable used has not been declared.
|
|
if (variables[name] == "") {
|
|
printf errors["variable-undefined"], name, imports[imports["length"] ":name"], FNR | "cat >&2"
|
|
exit 1
|
|
}
|
|
|
|
$0 = substr($0, 0, RSTART - 1) variables[name] substr($0, RSTART + RLENGTH, length($0))
|
|
}
|
|
}
|
|
|
|
# Line printing
|
|
# -------------
|
|
#
|
|
# This block contains line-printing rules, for results generated in the above
|
|
# blocks.
|
|
#
|
|
# > Match empty line. Consecutive empty lines do not print, and are instead
|
|
# > squashed down to a single line.
|
|
if (NF == 0) {
|
|
if ((newlines += 1) < 2) {
|
|
print
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
# > Print non-blank line, resetting the newline count, used above.
|
|
newlines = 0
|
|
print
|
|
|
|
}
|
|
|
|
# Import stack termination
|
|
# -------------------------
|
|
#
|
|
# This block contains termination rules for the import stack, as initialized in
|
|
# aforementioned blocks.
|
|
#
|
|
# When a file on the stack reaches EOF or error, the file is closed and the
|
|
# 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.
|
|
|
|
close(imports[imports["length"] ":name"])
|
|
delete processed[imports[imports["length"] ":name"]]
|
|
|
|
imports["length"] -= 1
|
|
|
|
}
|
|
|
|
# Cleanup
|
|
# -------
|
|
#
|
|
# This block contains cleanup operations on end of execution.
|
|
|
|
exit 0
|
|
|
|
}
|