CSS Preprocessor
This short python script is a preprocessor customized for CSS. You can use it to output CSS based on original CSS, where:
- @import directives are resolved by inlining the CSS file referenced in the directive (this means fewer CSS files need to be downloaded by a web page).
- #ifdef/#ifeq ... #else ... #endif are processed to select parts of the original CSS. Defines referenced in these conditional macros can supplied on the command line
- font sizes (expressed in em or percentages) can rescaled using a supplied coefficient. This can be useful when creating alternate sytlesheets that use larger fonts for better accesibility.
CSS Preprocessor Example
In the following example, we use css_preprocessor to generate two stylesheets from an original, then include both in a sample page.
python css_preprocessor.py < original.css > output1.css
python css_preprocessor.py -f 1.5 USEBORDERS COLOURSCHEME=DARK < original.css > output2.css
This page uses output1.css as the main stylesheet and output2.css as an alternate stylesheet. Use your browser's facility for switching to alternate stylesheets to see the differences.
The code for css_preprocessor.py is listed below:
css_preprocessor.py# css_preprocessor.py - preprocessor for CSS
# Copyright (c) 2008 Niall McCarroll
# Distributed under the MIT/X11 License (http://www.mccarroll.net/snippets/license.txt)
# functionality:
# * inline CSS included via @import commands
# * apply a scale factor to font-size commands for units px,em,%
# * apply simple preprocessor #ifdef <...> [#else <...>] #endif
# usage:
# python css_preprocessor.py -f scale < inpath.css > output.css
# where:
# inpath = path of input css file, scale = numeric factor to scale fonts, outpath = path of output file
import sys
import re
import os.path
font_pattern = re.compile(r'(.*)(font-size:|font:)\s*([0-9.]*)(em|px|%)(.*)')
def preprocessline(pattern_fns,line):
matches = []
for pattern in pattern_fns:
mat = pattern.match(line)
if mat:
matches.append((pattern_fns[pattern],mat))
matchpos = -1
matcher = None
processfn = None
for (fn,mat) in matches:
pre = mat.group(1)
pos = len(pre)
if matchpos == -1 or pos < matchpos:
matchpos = pos
matcher = mat
processfn = fn
if processfn:
return processfn(matcher,pattern_fns)
else:
return line
def font_change(mat,patternfns):
pre = mat.group(1)
sel = mat.group(2)
size = mat.group(3)
unit = mat.group(4)
post = mat.group(5)
if unit == '%':
if size == '100':
# setting font size to 100% may be part of a reset section
# do not rescale this
newsize = size
else:
newsize = str(int(int(size)*factor))
else:
newsize = "%.1f"% (float(size)*factor)
return pre + sel + newsize + unit + preprocessline(patternfns,post)
class preprocessor:
def __init__(self,fin,startdir,factor):
self.import_pattern = re.compile(r'\s*@import\s*url\("([^"]*)"\);\s*')
self.ifdef_pattern = re.compile(r'\s*#ifdef\s*([^\s]+)\s*')
self.ifeq_pattern = re.compile(r'\s*#if(eq|neq)\s*\(([^\s]+),([^\s]+)\)\s*')
self.else_pattern = re.compile(r'\s*#else\s*')
self.endif_pattern = re.compile(r'\s*#endif\s*')
self.scope = [True]
self.factor = factor
self.fin = fin
self.startdir = startdir
def inscope(self):
for s in self.scope:
if not s:
return False
return True
def preprocess(self):
print "/* created by css_preprocessor.py factor="+str(factor)
for define in defines:
if defines[define] == "":
print "\t" + define
else:
print "\t" + define + "=" + defines[define]
print "*/"
for line in self.fin:
line = line.rstrip("\n")
import_match = self.import_pattern.match(line)
ifdef_match = self.ifdef_pattern.match(line)
ifeq_match = self.ifeq_pattern.match(line)
else_match = self.else_pattern.match(line)
endif_match = self.endif_pattern.match(line)
if self.inscope() and import_match:
newpath = os.path.join(self.startdir,import_match.group(1))
print "/* begin "+line+" */"
importp = preprocessor(open(newpath,"r"),os.path.split(newpath)[0],self.factor)
importp.preprocess()
print "/* end "+line+" */"
elif ifdef_match:
define = ifdef_match.group(1)
self.scope.append(define in defines)
elif ifeq_match:
op = ifeq_match.group(1)
define = ifeq_match.group(2)
value = ifeq_match.group(3)
self.scope.append(define in defines and defines[define] == value)
if op == "neq":
self.scope[-1] = not self.scope[-1]
elif else_match:
self.scope.append(not self.scope.pop())
elif endif_match:
self.scope.pop()
if len(self.scope) == 0:
raise "Error - #endif with unmatched #ifdef"
elif self.inscope():
print preprocessline({font_pattern:font_change},line)
else:
pass
if len(self.scope) > 1:
raise "Error - #ifdef with no matching #ifdef"
from optparse import OptionParser
usage = "python css_preprocessor.py [-f <scale>] [NAME|NAME=VALUE]... < INPUT > OUTPUT"
version = "1.0"
parser = OptionParser(usage=usage,version=version)
parser.add_option("-f","--factor",dest="factor",type="float",help="scale factor for fonts")
(options,args) = parser.parse_args()
# read in macro definitions
defines = {}
for a in args:
l = a.split("=")
name = l[0]
value = ""
if len(l) > 1:
value = l[1]
defines[name] = value
# if a factor is supplied, override the default of 1
factor = 1
if options.factor:
factor = options.factor
p = preprocessor(sys.stdin,".",factor)
p.preprocess()