#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
"""css-convert <input-file> [<input-file> ...]

Read a simplified format for CSS files, strips comments and outputs a valid CSS
file.  You can specify multiple input files and the result will be a
concatenation of them.  Optionally runs the file through m4, and/or compress the
output into a more compact and harder-to-read format.

Specify '-' as a filename to read from stdin, or no filename at all.

The purpose is to be able to more easily edit CSS stylesheet with a comment
syntax that is easier to use, and to use indentation as a way of grouping rather
than requiring curly brackets to delimit sections.

Also, this script can produce a compressed version of the CSS file which is
harder to read by humans (e.g. humans who would want to rip off tricks off of
your design) and is smaller to download, thus saving bandwidth.

The simplified syntax is best illustrated by an example::

  div.header
    background-color: white

  // Make first paragraphs in bold.
  p.first
    font-style: bold

  p#special, div#special
    font-size: larger // larger fonts

Notes
-----

- we do not use curly brackets
- comments are parsed C++ style, so it's easy to comment entire regions
- you do not need to terminate lines with semicolons
- comment are stripped from the output file
- just like CSS, blocks cannot be nested

Note that if cssutils is installed in your system, we validate the CSS file
through its parser.

"""

# FIXME: todo add basic CSS1 validation and give out warnings.


__version__ = "Revision: 1.5 "
__author__ = "Martin Blais <blais@furius.ca>"


import sys, string
version = string.split(string.split(sys.version)[0], ".")[:2]
if map(int, version) < [2, 4]:
    raise SystemExit(
        "You must upgrade to Python 2.4 or higher to use this script.")

import re, StringIO
from subprocess import Popen, PIPE


def main():
    import optparse
    parser = optparse.OptionParser(__doc__.strip(), version=__version__)

    parser.add_option('-w', '--no-warnings', action='store_true',
                      help="Do not output warnings.")

    parser.add_option('-c', '--compressed', action='store_true',
                      help="Produce compressed output instead of "
                      "clean readable output.")

    parser.add_option('--debug', action='store_true',
                      help="Turn on debugging output.")

    parser.add_option('-o', '--output', action='store',
                      help="Specify output file (default is stdout).")

    parser.add_option('-V', '--validate', action='store_true',
                      help="Validate the CSS output (if cssutils "
                      "is available).")

    parser.add_option('-m', '--m4', action='store_true',
                      help="Run the input file through GNU m4 before "
                      "parsing it")

    opts, args = parser.parse_args()

    if not args:
        args = ['-'] # mark to read from stdin

    # open the output file at the beginning of the file
    of = opts.output and open(opts.output, 'w') or sys.stdout

    comre = re.compile('//.*$')
    wssre = re.compile('^\s+')
    endre = re.compile('^END')

    blocks = []
    curblock = None
    for fn in args:
        try:
            inf = (fn == '-' and sys.stdin) or open(fn, 'r')
        except IOError, e:
            raise SystemExit("Error opening file: %s" % e)

        if opts.m4:
            p = Popen('m4', shell=True,
                      stdin=PIPE, stdout=PIPE,
                      close_fds=True)
            txt = inf.read()
            stdout, stderr = p.communicate(txt)
	    inf = StringIO.StringIO(stdout)
	    if stderr:
	        print >>sys.stderr, stderr

        for noz, l in enumerate(inf):

            if endre.match(l):
                break

            # remove comments
            l = comre.sub('', l)

            # get rid of empty lines before parsing
            if l.strip() == '':
                continue

            # warn on the presence of semicolons
            if not opts.no_warnings:
                if ';' in l:
                    print >> sys.stderr, \
                          'Warning (line %d): semi-colon in string:' % (noz+1)
                    print >> sys.stderr, l

            ## Note: this was removed due to our wanting to be able to put a }
            ## in a value string.
            ##
            ##                 if '{' in l or '}' in l:
            ##                     print >> sys.stderr, \
            ## 'Warning (line %d): curly bracket in string:' % (noz+1)
            ##                     print >> sys.stderr, l

            l = l.replace(';', '')
            l = l.replace('{', '')
            l = l.replace('}', '')

            if opts.debug:
                print >> sys.stderr, '===', l,

            # check if this is a new block or an attribute of an existing block,
            # i.e. if it starts at the beginning of the line
            if wssre.match(l):
                # this is a name/value pair
                if curblock is None:
                    raise SystemExit('Error (line %d): no block present' %
                                     (noz+1))

                try:
                    name, value = l.split(':')
                    curblock[1].append( (name.strip(), value.strip()) )
                except ValueError:
                    raise SystemExit('Error (line %d): bad name/value pair:\n' %
                                     (noz+1) + '  ' + l)

            else:
                # this is a new block marker
                if curblock <> None:
                    blocks.append(curblock)
                curblock = (l.strip(), [])

        # get last block at the end of the file
        if curblock <> None:
            blocks.append(curblock)


    oss = StringIO.StringIO()
    for blockname, block in blocks:
        if blockname.startswith('@'):
            if not blockname.endswith(';'):
                blockname += ';'
            oss.write('%s\n' % blockname)
            assert not block
        else:
            if not opts.compressed:
                print >>oss, '%s {' % blockname
                for n, v in block:
                    print >>oss, '  %s: %s;' % (n, v)
                print >>oss, '}\n'
            else:
                oss.write('%s { %s }\n' % \
                          (blockname,
                           ' '.join(['%s: %s;' % (n, v) for n, v in block])))
    os = oss.getvalue()

    if opts.validate:
        try:
            from cssutils.cssparser import CSSParser

            p = CSSParser(raiseExceptions=True)
            try:
                css = p.parseString(os)
            except xml.dom.DOMException, e:
                etype, value, tb = sys.exc_info()
                print_exception( etype, value, tb, None, sys.stderr )
        except ImportError:
            pass

    of.write(os)

if __name__ == '__main__':
    main()
