fawkss/fawkss

175 lines
5.2 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 SCSS 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 `lastindex` returns the index of the last occurence of a substring
# > `s` in `str`, or -1 if the substring was not found.
function lastindex(str, s) {
match(str, ".*" s)
return max(RSTART + RLENGTH - 1, -1)
}
# > 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, chars) {
t = (chars != "") ? chars : ":space:"
match(str, "[" t "]*[^[" t "]]+[" t "]*")
return substr(str, 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;
}
# Initialization
# --------------
#
# This block contains logic for initializing global variables used across Fawkss.
BEGIN {
# Error messages used across Fawkss.
errors["unused-variable"] = "ERROR: Use of undeclared variable '%s' on line %d\n"
# Rule definitions. Each rule corresponds to a regular expression matching
# that rule on any given line.
rules["comment"] = "[ ]*//.*$"
rules["comment-exception"] = "['\"][^//]*//[^'\"]*['\"]"
rules["variable-name"] = "\\$[a-zA-Z0-9_]+"
rules["variable-define"] = "^[ ]*" rules["variable-name"] "[ ]*:"
}
# Rule definitions
# ----------------
#
# This block contains rule definitions used across Fawkss. A rule is defined as
# an exclusive match against a single line which always contines on to the next
# line. As such, rules are not composable.
#
# > 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.
$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
next
}
# Line manipulation
# -----------------
#
# This block contains line manipulation for rules defined in the previous section.
# Each line manipulation rule may modify the current line but should not continue
# to the next line, in order to allow for cascading effects.
#
# > 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.
$0 ~ rules["comment"] {
# 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"])) {
$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))
}
len = 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.
$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["unused-variable"], name, NR | "cat >&2"
exit
}
$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.
!NF {
if ((newlines += 1) < 2) {
print
}
next
}
# > Match non-black line. Resets newline count, used above.
{
newlines = 0
print
}