~doctormo/+junk/css-parser

« back to all changes in this revision

Viewing changes to csslavie/parse.py

  • Committer: Martin Owens
  • Date: 2014-07-13 21:48:23 UTC
  • Revision ID: doctormo@gmail.com-20140713214823-774n9vbbhw93zlbh
Move the files around and add some simple events

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
 
 
29
 
 
30
class FilterRejection(ValueError):
 
31
    """Raised when a filter fails to recognise the value"""
 
32
    pass
 
33
 
 
34
class CssValueFilters(list):
 
35
    """
 
36
    Filters allow values to be pre-packaged into the right format. This
 
37
    includes numbers, lengths (units) colours, urls, animations and so forth.
 
38
    Objectifying the value before it gets passed to the target object.
 
39
    """
 
40
    dd_f = "CSS Filter '%s' died with the error '%s', removed from filter list"
 
41
 
 
42
    def __init__(self, *filters):
 
43
        self.add(filters)
 
44
 
 
45
    def add(self, filters):
 
46
        for fil in filters:
 
47
            self.append(fil)
 
48
 
 
49
    def __call__(self, value):
 
50
        to_remove = []
 
51
        for fil in self:
 
52
            try:
 
53
                return fil(value)
 
54
            except FilterRejection:
 
55
                pass
 
56
            except Exception, error:
 
57
                to_remove.append(fil)
 
58
                name = getattr(fil, '__name__', str(fil))
 
59
                logging.error(self.dd_f % (name, str(error)))
 
60
        for fil in to_remove:
 
61
            self.remove(fil)
 
62
        return value
 
63
 
 
64
def number_filter(value):
 
65
    try:
 
66
        if '.' in value:
 
67
            return float(value)
 
68
        else:
 
69
            return int(value)
 
70
    except ValueError:
 
71
        raise FilterRejection("Not a Number")
 
72
        
 
73
 
 
74
GLOBAL_FILTERS = {
 
75
  number_filter,
 
76
}
 
77
 
 
78
 
 
79
def _block(string, f1, f2, s):
 
80
    """Find the start and end of a block in a string"""
 
81
    i = string.find(f1, s)
 
82
    j = string.find(f2, i)
 
83
    return (i, j)
 
84
 
 
85
 
 
86
def _parse(string, sep='=', eol='\n', vtype=str, ktype=str, start=0):
 
87
    """Parse any content between a start, end and seperator.
 
88
 
 
89
    sep   - seperator between a name and value pair
 
90
    eol   - An end of line marker to stop the parsing.
 
91
    t     - will return the given type, default string.
 
92
    start - Starting charicter number.
 
93
    vtype - Type or filter for each of the values before returning.
 
94
    ktype - Type or filter for each of the keys, if the function returns a list
 
95
            each immutable item in the list is a key with a duplicate of the value.
 
96
 
 
97
    """
 
98
    result = []
 
99
    while True:
 
100
        (i, j) = _block(string, sep, eol, start)
 
101
        if i < 0 or j < 0 or j < i:
 
102
            break
 
103
        name = string[start:i].strip()
 
104
        keys = ktype(name)
 
105
        value = vtype(string[i+1:j].strip())
 
106
        if not isinstance(keys, list):
 
107
            keys = [ keys ]
 
108
        for key in keys:
 
109
            result.append((key,value))
 
110
        start = j+1
 
111
    return result
 
112
 
 
113
def _remove(string, left='"""', right='"""'):
 
114
    """Removes comments from an input string (should be done first)."""
 
115
    start = 0
 
116
    while True:
 
117
        (i, j) = _block(string, left, right, start)
 
118
        if i < 0 or j < 0 or j < i:
 
119
            break
 
120
        string = string[:i] + string[j+2:]
 
121
        start = i
 
122
    return string
 
123
 
 
124
def _parse_names(content):
 
125
    result = []
 
126
    for name in content.strip().split(' '):
 
127
        for char in '.#:':
 
128
            name = name.replace(char, ' '+char)
 
129
        result.append( name.strip() )
 
130
    return result
 
131
 
 
132
def _parse_css(content, filters=None):
 
133
    """Parse css formated content"""
 
134
    filters = filters or CssValueFilters(*GLOBAL_FILTERS)
 
135
    content = _remove(content, '/*', '*/')
 
136
    content = _remove(content, '//', '\n')
 
137
    return _parse(content, '{', '}',
 
138
        ktype=lambda c: [ _parse_names(p) for p in c.split(',') ],
 
139
        vtype=lambda c: dict( _parse(c,":",";", vtype=filters))
 
140
    )
 
141
 
 
142
 
 
143
def CssParser(filename=None):
 
144
    """Returns a style sheet for the given filename"""
 
145
    if filename:
 
146
        with open(filename, 'r') as fhl:
 
147
            return StyleSheet(fhl.read())
 
148
    return StyleSheet()
 
149
 
 
150
 
 
151
def get_weight(name):
 
152
    """Returns a weight based on the name construction,
 
153
 
 
154
     ! this might be standardised somewhere, but I didn't look it up.
 
155
    """
 
156
    return name.count(".") + name.count(" ") * 2 + name.count("#") * 4 + name.count(">") * 8
 
157
 
 
158
class StyleSheet(object):
 
159
    """This stylesheet object allows one to 'attach' objects to css, this css
 
160
    will use the attributes in the object and update their properties."""
 
161
    def __init__(self, content=None, name='__name__', oid='name', cls='classes', filters=None):
 
162
        self._attr_name = name
 
163
        self._attr_oid  = oid
 
164
        self._attr_cls  = cls
 
165
 
 
166
        self.styles = []
 
167
        self.filters = CssValueFilters(*(filters or []))
 
168
        self.filters.add(GLOBAL_FILTERS)
 
169
 
 
170
        if content:
 
171
            self.styles = _parse_css(content, filters=self.filters)
 
172
 
 
173
        _i = defaultdict(list)
 
174
 
 
175
        for names, style in self.styles:
 
176
            for name in names:
 
177
                _i[name].append(style)
 
178
 
 
179
        # Weigh up the names and sort them by weight here
 
180
        self._index = OrderedDict(sorted(_i.iteritems(), key=lambda x: get_weight(x[0])))
 
181
 
 
182
    def attach(self, obj):
 
183
        names = Names()
 
184
        names.add( getattr(type(obj), self._attr_name, None) )
 
185
        names.add( getattr(obj, self._attr_oid, None), '#' )
 
186
        for c in getattr(obj, self._attr_cls, []) or []:
 
187
            names.add(c, '.')
 
188
        for (ns, style) in self._index.items():
 
189
            if names.match(ns):
 
190
                for s in style:
 
191
                    obj.add_to(s)
 
192
        # Attach signal here for state updates
 
193
 
 
194
class Names(set):
 
195
    def add(self, name, sep=''):
 
196
        if name != None:
 
197
            set.add(self, sep + str(name).lower())
 
198
 
 
199
    def match(self, sublist):
 
200
        if type(sublist) is str:
 
201
            sublist = sublist.split(' ')
 
202
        for name in sublist:
 
203
            if name not in self:
 
204
                return False
 
205
        #print "'%s' was in %s" % (str(sublist), str(self))
 
206
        return True
 
207
 
 
208
 
 
209
 
 
210