~doctormo/+junk/css-parser

« back to all changes in this revision

Viewing changes to css.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
 
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