CSS Preprocessor

This short python script is a preprocessor customized for CSS. You can use it to output CSS based on original CSS, where:

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()
 
Anti-spam check
Leave a comment