~doctormo/+junk/css-parser

« back to all changes in this revision

Viewing changes to css.py

  • Committer: Martin Owens
  • Date: 2014-07-13 19:16:11 UTC
  • Revision ID: doctormo@gmail.com-20140713191611-48dkgp1dklvh8bq2
Inital addition of working css files

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Copyright 2012-2014 Martin Owens <doctormo@gmail.com>
 
3
# Copyright      2014 Ian Denhardt <ian@zenhack.net>
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify
 
6
#  it under the terms of the GNU General Public License as published by
 
7
#  the Free Software Foundation, either version 3 of the License, or
 
8
#  (at your option) any later version.
 
9
#
 
10
#  This program is distributed in the hope that it will be useful,
 
11
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
#  GNU General Public License for more details.
 
14
#
 
15
#  You should have received a copy of the GNU General Public License
 
16
#  along with this program.  If not, see <http://www.gnu.org/licenses/>
 
17
#
 
18
"""
 
19
Take in CSS text and produce a property map to be applied to items.
 
20
"""
 
21
 
 
22
import re
 
23
import os
 
24
import sys
 
25
import logging
 
26
 
 
27
from collections import defaultdict, OrderedDict
 
28
from itertools import combinations
 
29
 
 
30
 
 
31
 
 
32
class FilterRejection(ValueError):
 
33
    """Raised when a filter fails to recognise the value"""
 
34
    pass
 
35
 
 
36
class CssValueFilters(list):
 
37
    """
 
38
    Filters allow values to be pre-packaged into the right format. This
 
39
    includes numbers, lengths (units) colours, urls, animations and so forth.
 
40
    Objectifying the value before it gets passed to the target object.
 
41
    """
 
42
    dd_f = "CSS Filter '%s' died with the error '%s', removed from filter list"
 
43
 
 
44
    def __init__(self, *filters):
 
45
        self.add(filters)
 
46
 
 
47
    def add(self, filters):
 
48
        for fil in filters:
 
49
            self.append(fil)
 
50
 
 
51
    def __call__(self, value):
 
52
        to_remove = []
 
53
        for fil in self:
 
54
            try:
 
55
                return fil(value)
 
56
            except FilterRejection:
 
57
                pass
 
58
            except Exception, error:
 
59
                to_remove.append(fil)
 
60
                name = getattr(fil, '__name__', str(fil))
 
61
                logging.error(self.dd_f % (name, str(error)))
 
62
        for fil in to_remove:
 
63
            self.remove(fil)
 
64
        return value
 
65
 
 
66
def number_filter(value):
 
67
    try:
 
68
        if '.' in value:
 
69
            return float(value)
 
70
        else:
 
71
            return int(value)
 
72
    except ValueError:
 
73
        raise FilterRejection("Not a Number")
 
74
        
 
75
 
 
76
GLOBAL_FILTERS = {
 
77
  number_filter,
 
78
}
 
79
 
 
80
 
 
81
def _block(string, f1, f2, s):
 
82
    """Find the start and end of a block in a string"""
 
83
    i = string.find(f1, s)
 
84
    j = string.find(f2, i)
 
85
    return (i, j)
 
86
 
 
87
 
 
88
def _parse(string, sep='=', eol='\n', vtype=str, ktype=str, start=0):
 
89
    """Parse any content between a start, end and seperator.
 
90
 
 
91
    sep   - seperator between a name and value pair
 
92
    eol   - An end of line marker to stop the parsing.
 
93
    t     - will return the given type, default string.
 
94
    start - Starting charicter number.
 
95
    vtype - Type or filter for each of the values before returning.
 
96
    ktype - Type or filter for each of the keys, if the function returns a list
 
97
            each immutable item in the list is a key with a duplicate of the value.
 
98
 
 
99
    """
 
100
    result = []
 
101
    while True:
 
102
        (i, j) = _block(string, sep, eol, start)
 
103
        if i < 0 or j < 0 or j < i:
 
104
            break
 
105
        name = string[start:i].strip()
 
106
        keys = ktype(name)
 
107
        value = vtype(string[i+1:j].strip())
 
108
        if not isinstance(keys, list):
 
109
            keys = [ keys ]
 
110
        for key in keys:
 
111
            result.append((key,value))
 
112
        start = j+1
 
113
    return result
 
114
 
 
115
def _remove(string, left='"""', right='"""'):
 
116
    """Removes comments from an input string (should be done first)."""
 
117
    start = 0
 
118
    while True:
 
119
        (i, j) = _block(string, left, right, start)
 
120
        if i < 0 or j < 0 or j < i:
 
121
            break
 
122
        string = string[:i] + string[j+2:]
 
123
        start = i
 
124
    return string
 
125
 
 
126
def _parse_names(content):
 
127
    result = []
 
128
    for name in content.strip().split(' '):
 
129
        for char in '.#:':
 
130
            name = name.replace(char, ' '+char)
 
131
        result.append( name.strip() )
 
132
    return result
 
133
 
 
134
def _parse_css(content, filters=None):
 
135
    """Parse css formated content"""
 
136
    filters = filters or CssValueFilters(*GLOBAL_FILTERS)
 
137
    content = _remove(content, '/*', '*/')
 
138
    content = _remove(content, '//', '\n')
 
139
    return _parse(content, '{', '}',
 
140
        ktype=lambda c: [ _parse_names(p) for p in c.split(',') ],
 
141
        vtype=lambda c: dict( _parse(c,":",";", vtype=filters))
 
142
    )
 
143
 
 
144
 
 
145
def CssParser(filename=None):
 
146
    """Returns a style sheet for the given filename"""
 
147
    if filename:
 
148
        with open(filename, 'r') as fhl:
 
149
            return StyleSheet(fhl.read())
 
150
    return StyleSheet()
 
151
 
 
152
 
 
153
def get_weight(name):
 
154
    """Returns a weight based on the name construction,
 
155
 
 
156
     ! this might be standardised somewhere, but I didn't look it up.
 
157
    """
 
158
    return name.count(" ") + name.count("#") + name.count(".") + name.count(">")
 
159
 
 
160
class StyleSheet(object):
 
161
    """This stylesheet object allows one to 'attach' objects to css, this css
 
162
    will use the attributes in the object and update their properties."""
 
163
    def __init__(self, content=None, name='__name__', oid='name', cls='classes', filters=None):
 
164
        self._attr_name = name
 
165
        self._attr_oid  = oid
 
166
        self._attr_cls  = cls
 
167
 
 
168
        self.styles = []
 
169
        self.filters = CssValueFilters(*(filters or []))
 
170
        self.filters.add(GLOBAL_FILTERS)
 
171
 
 
172
        if content:
 
173
            self.styles = _parse_css(content, filters=self.filters)
 
174
 
 
175
        _i = defaultdict(list)
 
176
 
 
177
        for names, style in self.styles:
 
178
            for name in names:
 
179
                _i[name].append(style)
 
180
 
 
181
        # Weigh up the names and sort them by weight here
 
182
        self._index = OrderedDict(sorted(_i.iteritems(), key=lambda x: get_weight(x[0])))
 
183
 
 
184
    def attach(self, obj):
 
185
        names = Names()
 
186
        names.add( getattr(type(obj), self._attr_name, None) )
 
187
        names.add( getattr(obj, self._attr_oid, None), '#' )
 
188
        for c in getattr(obj, self._attr_cls, []):
 
189
            names.add(c, '.')
 
190
        for (ns, style) in self._index.items():
 
191
            if names.match(ns):
 
192
                for s in style:
 
193
                    obj.add_to(s)
 
194
        # Attach signal here for state updates
 
195
 
 
196
class Names(set):
 
197
    def add(self, name, sep=''):
 
198
        if name != None:
 
199
            set.add(self, sep + str(name).lower())
 
200
 
 
201
    def match(self, sublist):
 
202
        if type(sublist) is str:
 
203
            sublist = sublist.split(' ')
 
204
        for name in sublist:
 
205
            if name not in self:
 
206
                return False
 
207
        #print "'%s' was in %s" % (str(sublist), str(self))
 
208
        return True
 
209
 
 
210
 
 
211
 
 
212