~ubuntu-branches/ubuntu/jaunty/calibre/jaunty-backports

« back to all changes in this revision

Viewing changes to src/cssutils/util.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-01-20 17:14:02 UTC
  • Revision ID: james.westby@ubuntu.com-20090120171402-8y3znf6nokwqe80k
Tags: upstream-0.4.125+dfsg
ImportĀ upstreamĀ versionĀ 0.4.125+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""base classes and helper functions for css and stylesheets packages
 
2
"""
 
3
__all__ = []
 
4
__docformat__ = 'restructuredtext'
 
5
__version__ = '$Id: util.py 1453 2008-09-08 20:57:19Z cthedot $'
 
6
 
 
7
import codecs
 
8
from itertools import ifilter
 
9
import types
 
10
import urllib2
 
11
import xml.dom
 
12
 
 
13
from helper import normalize
 
14
import tokenize2
 
15
import cssutils
 
16
import encutils
 
17
 
 
18
class Base(object):
 
19
    """
 
20
    Base class for most CSS and StyleSheets classes
 
21
 
 
22
    **Superceded by Base2 which is used for new seq handling class.**
 
23
    See cssutils.util.Base2
 
24
 
 
25
    Contains helper methods for inheriting classes helping parsing
 
26
 
 
27
    ``_normalize`` is static as used by Preferences.
 
28
    """
 
29
    __tokenizer2 = tokenize2.Tokenizer()
 
30
 
 
31
    _log = cssutils.log
 
32
    _prods = tokenize2.CSSProductions
 
33
 
 
34
    # for more on shorthand properties see
 
35
    # http://www.dustindiaz.com/css-shorthand/
 
36
    # format: shorthand: [(propname, mandatorycheck?)*]
 
37
    _SHORTHANDPROPERTIES = {
 
38
            u'background': [],
 
39
            u'background-position': [],
 
40
            u'border': [],
 
41
            u'border-left': [],
 
42
            u'border-right': [],
 
43
            u'border-top': [],
 
44
            u'border-bottom': [],
 
45
            #u'border-color': [], # list or single but same values
 
46
            #u'border-style': [], # list or single but same values
 
47
            #u'border-width': [], # list or single but same values
 
48
            u'cue': [],
 
49
            u'font': [],
 
50
            u'list-style': [],
 
51
            #u'margin': [], # list or single but same values
 
52
            u'outline': [],
 
53
            #u'padding': [], # list or single but same values
 
54
            u'pause': []
 
55
            }
 
56
 
 
57
    @staticmethod
 
58
    def _normalize(x):
 
59
        """
 
60
        normalizes x, namely:
 
61
 
 
62
        - remove any \ before non unicode sequences (0-9a-zA-Z) so for
 
63
          x=="c\olor\" return "color" (unicode escape sequences should have
 
64
          been resolved by the tokenizer already)
 
65
        - lowercase
 
66
        """
 
67
        return normalize(x)
 
68
 
 
69
    def _checkReadonly(self):
 
70
        "raises xml.dom.NoModificationAllowedErr if rule/... is readonly"
 
71
        if hasattr(self, '_readonly') and self._readonly:
 
72
            raise xml.dom.NoModificationAllowedErr(
 
73
                u'%s is readonly.' % self.__class__)
 
74
            return True
 
75
        return False
 
76
 
 
77
    def _splitNamespacesOff(self, text_namespaces_tuple):
 
78
        """
 
79
        returns tuple (text, dict-of-namespaces) or if no namespaces are
 
80
        in cssText returns (cssText, {})
 
81
 
 
82
        used in Selector, SelectorList, CSSStyleRule, CSSMediaRule and
 
83
        CSSStyleSheet
 
84
        """
 
85
        if isinstance(text_namespaces_tuple, tuple):
 
86
            return text_namespaces_tuple[0], _SimpleNamespaces(self._log,
 
87
                                                    text_namespaces_tuple[1])
 
88
        else:
 
89
            return text_namespaces_tuple, _SimpleNamespaces(log=self._log)
 
90
 
 
91
    def _tokenize2(self, textortokens):
 
92
        """
 
93
        returns tokens of textortokens which may already be tokens in which
 
94
        case simply returns input
 
95
        """
 
96
        if not textortokens:
 
97
            return None
 
98
        elif isinstance(textortokens, basestring):
 
99
            # needs to be tokenized
 
100
            return self.__tokenizer2.tokenize(
 
101
                 textortokens)
 
102
        elif types.GeneratorType == type(textortokens):
 
103
            # already tokenized
 
104
            return textortokens
 
105
        elif isinstance(textortokens, tuple):
 
106
            # a single token (like a comment)
 
107
            return [textortokens]
 
108
        else:
 
109
            # already tokenized but return generator
 
110
            return (x for x in textortokens)
 
111
 
 
112
    def _nexttoken(self, tokenizer, default=None):
 
113
        "returns next token in generator tokenizer or the default value"
 
114
        try:
 
115
            return tokenizer.next()
 
116
        except (StopIteration, AttributeError):
 
117
            return default
 
118
 
 
119
    def _type(self, token):
 
120
        "returns type of Tokenizer token"
 
121
        if token:
 
122
            return token[0]
 
123
        else:
 
124
            return None
 
125
 
 
126
    def _tokenvalue(self, token, normalize=False):
 
127
        "returns value of Tokenizer token"
 
128
        if token and normalize:
 
129
            return Base._normalize(token[1])
 
130
        elif token:
 
131
            return token[1]
 
132
        else:
 
133
            return None
 
134
 
 
135
    def _stringtokenvalue(self, token):
 
136
        """
 
137
        for STRING returns the actual content without surrounding "" or ''
 
138
        and without respective escapes, e.g.::
 
139
 
 
140
             "with \" char" => with " char
 
141
        """
 
142
        if token:
 
143
            value = token[1]
 
144
            return value.replace('\\'+value[0], value[0])[1:-1]
 
145
        else:
 
146
            return None
 
147
 
 
148
    def _uritokenvalue(self, token):
 
149
        """
 
150
        for URI returns the actual content without surrounding url()
 
151
        or url(""), url('') and without respective escapes, e.g.::
 
152
 
 
153
             url("\"") => "
 
154
        """
 
155
        if token:
 
156
            value = token[1][4:-1].strip()
 
157
            if value and (value[0] in '\'"') and (value[0] == value[-1]):
 
158
                # a string "..." or '...'
 
159
                value = value.replace('\\'+value[0], value[0])[1:-1]
 
160
            return value
 
161
        else:
 
162
            return None
 
163
 
 
164
    def _tokensupto2(self,
 
165
                     tokenizer,
 
166
                     starttoken=None,
 
167
                     blockstartonly=False, # {
 
168
                     blockendonly=False, # }
 
169
                     mediaendonly=False,
 
170
                     importmediaqueryendonly=False, # ; or STRING
 
171
                     mediaqueryendonly=False, # { or STRING
 
172
                     semicolon=False, # ;
 
173
                     propertynameendonly=False, # :
 
174
                     propertyvalueendonly=False, # ! ; }
 
175
                     propertypriorityendonly=False, # ; }
 
176
                     selectorattendonly=False, # ]
 
177
                     funcendonly=False, # )
 
178
                     listseponly=False, # ,
 
179
                     separateEnd=False # returns (resulttokens, endtoken)
 
180
                     ):
 
181
        """
 
182
        returns tokens upto end of atrule and end index
 
183
        end is defined by parameters, might be ; } ) or other
 
184
 
 
185
        default looks for ending "}" and ";"
 
186
        """
 
187
        ends = u';}'
 
188
        endtypes = ()
 
189
        brace = bracket = parant = 0 # {}, [], ()
 
190
 
 
191
        if blockstartonly: # {
 
192
            ends = u'{'
 
193
            brace = -1 # set to 0 with first {
 
194
        elif blockendonly: # }
 
195
            ends = u'}'
 
196
            brace = 1
 
197
        elif mediaendonly: # }
 
198
            ends = u'}'
 
199
            brace = 1 # rules } and mediarules }
 
200
        elif importmediaqueryendonly:
 
201
            # end of mediaquery which may be ; or STRING
 
202
            ends = u';'
 
203
            endtypes = ('STRING',)
 
204
        elif mediaqueryendonly:
 
205
            # end of mediaquery which may be { or STRING
 
206
            # special case, see below
 
207
            ends = u'{'
 
208
            brace = -1 # set to 0 with first {
 
209
            endtypes = ('STRING',)
 
210
        elif semicolon:
 
211
            ends = u';'
 
212
        elif propertynameendonly: # : and ; in case of an error
 
213
            ends = u':;'
 
214
        elif propertyvalueendonly: # ; or !important
 
215
            ends = u';!'
 
216
        elif propertypriorityendonly: # ;
 
217
            ends = u';'
 
218
        elif selectorattendonly: # ]
 
219
            ends = u']'
 
220
            if starttoken and self._tokenvalue(starttoken) == u'[':
 
221
                bracket = 1
 
222
        elif funcendonly: # )
 
223
            ends = u')'
 
224
            parant = 1
 
225
        elif listseponly: # ,
 
226
            ends = u','
 
227
 
 
228
        resulttokens = []
 
229
        if starttoken:
 
230
            resulttokens.append(starttoken)
 
231
        if tokenizer:
 
232
            for token in tokenizer:
 
233
                typ, val, line, col = token
 
234
                if 'EOF' == typ:
 
235
                    resulttokens.append(token)
 
236
                    break
 
237
                if u'{' == val:
 
238
                    brace += 1
 
239
                elif u'}' == val:
 
240
                    brace -= 1
 
241
                elif u'[' == val:
 
242
                    bracket += 1
 
243
                elif u']' == val:
 
244
                    bracket -= 1
 
245
                # function( or single (
 
246
                elif u'(' == val or \
 
247
                     Base._prods.FUNCTION == typ:
 
248
                    parant += 1
 
249
                elif u')' == val:
 
250
                    parant -= 1
 
251
 
 
252
                resulttokens.append(token)
 
253
 
 
254
                if (brace == bracket == parant == 0) and (
 
255
                    val in ends or typ in endtypes):
 
256
                    break
 
257
                elif mediaqueryendonly and brace == -1 and (
 
258
                     bracket == parant == 0) and typ in endtypes:
 
259
                     # mediaqueryendonly with STRING
 
260
                    break
 
261
 
 
262
        if separateEnd:
 
263
            # TODO: use this method as generator, then this makes sense
 
264
            if resulttokens:
 
265
                return resulttokens[:-1], resulttokens[-1]
 
266
            else:
 
267
                return resulttokens, None
 
268
        else:
 
269
            return resulttokens
 
270
 
 
271
    def _valuestr(self, t):
 
272
        """
 
273
        returns string value of t (t may be a string, a list of token tuples
 
274
        or a single tuple in format (type, value, line, col).
 
275
        Mainly used to get a string value of t for error messages.
 
276
        """
 
277
        if not t:
 
278
            return u''
 
279
        elif isinstance(t, basestring):
 
280
            return t
 
281
        else:
 
282
            return u''.join([x[1] for x in t])
 
283
 
 
284
    def _adddefaultproductions(self, productions, new=None):
 
285
        """
 
286
        adds default productions if not already present, used by
 
287
        _parse only
 
288
 
 
289
        each production should return the next expected token
 
290
        normaly a name like "uri" or "EOF"
 
291
        some have no expectation like S or COMMENT, so simply return
 
292
        the current value of self.__expected
 
293
        """
 
294
        def ATKEYWORD(expected, seq, token, tokenizer=None):
 
295
            "default impl for unexpected @rule"
 
296
            if expected != 'EOF':
 
297
                # TODO: parentStyleSheet=self
 
298
                rule = cssutils.css.CSSUnknownRule()
 
299
                rule.cssText = self._tokensupto2(tokenizer, token)
 
300
                if rule.wellformed:
 
301
                    seq.append(rule)
 
302
                return expected
 
303
            else:
 
304
                new['wellformed'] = False
 
305
                self._log.error(u'Expected EOF.', token=token)
 
306
                return expected
 
307
 
 
308
        def COMMENT(expected, seq, token, tokenizer=None):
 
309
            "default implementation for COMMENT token adds CSSCommentRule"
 
310
            seq.append(cssutils.css.CSSComment([token]))
 
311
            return expected
 
312
 
 
313
        def S(expected, seq, token, tokenizer=None):
 
314
            "default implementation for S token, does nothing"
 
315
            return expected
 
316
 
 
317
        def EOF(expected=None, seq=None, token=None, tokenizer=None):
 
318
            "default implementation for EOF token"
 
319
            return 'EOF'
 
320
 
 
321
        p = {'ATKEYWORD': ATKEYWORD,
 
322
             'COMMENT': COMMENT,
 
323
             'S': S,
 
324
             'EOF': EOF # only available if fullsheet
 
325
             }
 
326
        p.update(productions)
 
327
        return p
 
328
 
 
329
    def _parse(self, expected, seq, tokenizer, productions, default=None,
 
330
               new=None, initialtoken=None):
 
331
        """
 
332
        puts parsed tokens in seq by calling a production with
 
333
            (seq, tokenizer, token)
 
334
 
 
335
        expected
 
336
            a name what token or value is expected next, e.g. 'uri'
 
337
        seq
 
338
            to add rules etc to
 
339
        tokenizer
 
340
            call tokenizer.next() to get next token
 
341
        productions
 
342
            callbacks {tokentype: callback}
 
343
        default
 
344
            default callback if tokentype not in productions
 
345
        new
 
346
            used to init default productions
 
347
        initialtoken
 
348
            will be used together with tokenizer running 1st this token
 
349
            and then all tokens in tokenizer
 
350
 
 
351
        returns (wellformed, expected) which the last prod might have set
 
352
        """
 
353
        wellformed = True
 
354
        
 
355
        if initialtoken:
 
356
            # add initialtoken to tokenizer
 
357
            def tokens():
 
358
                "Build new tokenizer including initialtoken"
 
359
                yield initialtoken
 
360
                for item in tokenizer:
 
361
                    yield item
 
362
            fulltokenizer = (t for t in tokens())
 
363
        else:
 
364
            fulltokenizer = tokenizer
 
365
                
 
366
        if fulltokenizer:
 
367
            prods = self._adddefaultproductions(productions, new)
 
368
            for token in fulltokenizer:
 
369
                p = prods.get(token[0], default)
 
370
                if p:
 
371
                    expected = p(expected, seq, token, tokenizer)
 
372
                else:
 
373
                    wellformed = False
 
374
                    self._log.error(u'Unexpected token (%s, %s, %s, %s)' % token)
 
375
        return wellformed, expected
 
376
 
 
377
 
 
378
class Base2(Base):
 
379
    """
 
380
    Base class for new seq handling, used by Selector for now only
 
381
    """
 
382
    def __init__(self):
 
383
        self._seq = Seq()
 
384
 
 
385
    def _setSeq(self, newseq):
 
386
        """
 
387
        sets newseq and makes it readonly
 
388
        """
 
389
        newseq._readonly = True
 
390
        self._seq = newseq
 
391
 
 
392
    seq = property(lambda self: self._seq, doc="seq for most classes")
 
393
 
 
394
    def _tempSeq(self, readonly=False):
 
395
        "get a writeable Seq() which is added later"
 
396
        return Seq(readonly=readonly)
 
397
 
 
398
    def _adddefaultproductions(self, productions, new=None):
 
399
        """
 
400
        adds default productions if not already present, used by
 
401
        _parse only
 
402
 
 
403
        each production should return the next expected token
 
404
        normaly a name like "uri" or "EOF"
 
405
        some have no expectation like S or COMMENT, so simply return
 
406
        the current value of self.__expected
 
407
        """
 
408
        def ATKEYWORD(expected, seq, token, tokenizer=None):
 
409
            "default impl for unexpected @rule"
 
410
            if expected != 'EOF':
 
411
                # TODO: parentStyleSheet=self
 
412
                rule = cssutils.css.CSSUnknownRule()
 
413
                rule.cssText = self._tokensupto2(tokenizer, token)
 
414
                if rule.wellformed:
 
415
                    seq.append(rule, cssutils.css.CSSRule.UNKNOWN_RULE,
 
416
                               line=token[2], col=token[3])
 
417
                return expected
 
418
            else:
 
419
                new['wellformed'] = False
 
420
                self._log.error(u'Expected EOF.', token=token)
 
421
                return expected
 
422
 
 
423
        def COMMENT(expected, seq, token, tokenizer=None):
 
424
            "default impl, adds CSSCommentRule if not token == EOF"
 
425
            if expected == 'EOF':
 
426
                new['wellformed'] = False
 
427
                self._log.error(u'Expected EOF but found comment.', token=token)
 
428
            seq.append(cssutils.css.CSSComment([token]), 'COMMENT')
 
429
            return expected
 
430
 
 
431
        def S(expected, seq, token, tokenizer=None):
 
432
            "default impl, does nothing if not token == EOF"
 
433
            if expected == 'EOF':
 
434
                new['wellformed'] = False
 
435
                self._log.error(u'Expected EOF but found whitespace.', token=token)
 
436
            return expected
 
437
 
 
438
        def EOF(expected=None, seq=None, token=None, tokenizer=None):
 
439
            "default implementation for EOF token"
 
440
            return 'EOF'
 
441
 
 
442
        defaultproductions = {'ATKEYWORD': ATKEYWORD,
 
443
             'COMMENT': COMMENT,
 
444
             'S': S,
 
445
             'EOF': EOF # only available if fullsheet
 
446
             }
 
447
        defaultproductions.update(productions)
 
448
        return defaultproductions
 
449
 
 
450
 
 
451
class Seq(object):
 
452
    """
 
453
    property seq of Base2 inheriting classes, holds a list of Item objects.
 
454
 
 
455
    used only by Selector for now
 
456
 
 
457
    is normally readonly, only writable during parsing
 
458
    """
 
459
    def __init__(self, readonly=True):
 
460
        """
 
461
        only way to write to a Seq is to initialize it with new items
 
462
        each itemtuple has (value, type, line) where line is optional
 
463
        """
 
464
        self._seq = []
 
465
        self._readonly = readonly
 
466
 
 
467
    def __delitem__(self, i):
 
468
        del self._seq[i]
 
469
 
 
470
    def __getitem__(self, i):
 
471
        return self._seq[i]
 
472
 
 
473
    def __setitem__(self, i, (val, typ, line, col)):
 
474
        self._seq[i] = Item(val, typ, line, col)
 
475
 
 
476
    def __iter__(self):
 
477
        return iter(self._seq)
 
478
 
 
479
    def __len__(self):
 
480
        return len(self._seq)
 
481
 
 
482
    def append(self, val, typ, line=None, col=None):
 
483
        "if not readonly add new Item()"
 
484
        if self._readonly:
 
485
            raise AttributeError('Seq is readonly.')
 
486
        else:
 
487
            self._seq.append(Item(val, typ, line, col))
 
488
 
 
489
    def appendItem(self, item):
 
490
        "if not readonly add item which must be an Item"
 
491
        if self._readonly:
 
492
            raise AttributeError('Seq is readonly.')
 
493
        else:
 
494
            self._seq.append(item)
 
495
 
 
496
    def replace(self, index=-1, val=None, typ=None, line=None, col=None):
 
497
        """
 
498
        if not readonly replace Item at index with new Item or
 
499
        simply replace value or type
 
500
        """
 
501
        if self._readonly:
 
502
            raise AttributeError('Seq is readonly.')
 
503
        else:
 
504
            self._seq[index] = Item(val, typ, line, col)
 
505
 
 
506
    def appendToVal(self, val=None, index=-1):
 
507
        """
 
508
        if not readonly append to Item's value at index
 
509
        """
 
510
        if self._readonly:
 
511
            raise AttributeError('Seq is readonly.')
 
512
        else:
 
513
            old = self._seq[index]
 
514
            self._seq[index] = Item(old.value + val, old.type, 
 
515
                                    old.line, old.col)
 
516
 
 
517
    def __repr__(self):
 
518
        "returns a repr same as a list of tuples of (value, type)"
 
519
        return u'cssutils.%s.%s([\n    %s])' % (self.__module__,
 
520
                                          self.__class__.__name__,
 
521
            u',\n    '.join([u'%r' % item for item in self._seq]
 
522
            ))
 
523
    def __str__(self):
 
524
        vals = []
 
525
        for v in self:
 
526
            if isinstance(v.value, basestring):
 
527
                vals.append(v.value)
 
528
            elif type(v) == tuple:
 
529
                vals.append(v.value[1])
 
530
            else:
 
531
                vals.append(str(v))
 
532
        
 
533
        return "<cssutils.%s.%s object length=%r valuestring=%r at 0x%x>" % (
 
534
                self.__module__, self.__class__.__name__, len(self), 
 
535
                u''.join(vals), id(self))
 
536
 
 
537
class Item(object):
 
538
    """
 
539
    an item in the seq list of classes (successor to tuple items in old seq)
 
540
 
 
541
    each item has attributes:
 
542
 
 
543
    type
 
544
        a sematic type like "element", "attribute"
 
545
    value
 
546
        the actual value which may be a string, number etc or an instance
 
547
        of e.g. a CSSComment
 
548
    *line*
 
549
        **NOT IMPLEMENTED YET, may contain the line in the source later**
 
550
    """
 
551
    def __init__(self, value, type, line=None, col=None):
 
552
        self.__value = value
 
553
        self.__type = type
 
554
        self.__line = line
 
555
        self.__col = col
 
556
 
 
557
    type = property(lambda self: self.__type)
 
558
    value = property(lambda self: self.__value)
 
559
    line = property(lambda self: self.__line)
 
560
    col = property(lambda self: self.__col)
 
561
 
 
562
    def __repr__(self):
 
563
        return "%s.%s(value=%r, type=%r, line=%r, col=%r)" % (
 
564
                self.__module__, self.__class__.__name__,
 
565
                self.__value, self.__type, self.__line, self.__col)
 
566
 
 
567
 
 
568
class ListSeq(object):
 
569
    """
 
570
    (EXPERIMENTAL)
 
571
    A base class used for list classes like css.SelectorList or
 
572
    stylesheets.MediaList
 
573
 
 
574
    adds list like behaviour running on inhering class' property ``seq``
 
575
 
 
576
    - item in x => bool
 
577
    - len(x) => integer
 
578
    - get, set and del x[i]
 
579
    - for item in x
 
580
    - append(item)
 
581
 
 
582
    some methods must be overwritten in inheriting class
 
583
    """
 
584
    def __init__(self):
 
585
        self.seq = [] # does not need to use ``Seq`` as simple list only
 
586
 
 
587
    def __contains__(self, item):
 
588
        return item in self.seq
 
589
 
 
590
    def __delitem__(self, index):
 
591
        del self.seq[index]
 
592
 
 
593
    def __getitem__(self, index):
 
594
        return self.seq[index]
 
595
 
 
596
    def __iter__(self):
 
597
        def gen():
 
598
            for x in self.seq:
 
599
                yield x
 
600
        return gen()
 
601
 
 
602
    def __len__(self):
 
603
        return len(self.seq)
 
604
 
 
605
    def __setitem__(self, index, item):
 
606
        "must be overwritten"
 
607
        raise NotImplementedError
 
608
 
 
609
    def append(self, item):
 
610
        "must be overwritten"
 
611
        raise NotImplementedError
 
612
 
 
613
 
 
614
class _Namespaces(object):
 
615
    """
 
616
    A dictionary like wrapper for @namespace rules used in a CSSStyleSheet.
 
617
    Works on effective namespaces, so e.g. if::
 
618
 
 
619
        @namespace p1 "uri";
 
620
        @namespace p2 "uri";
 
621
 
 
622
    only the second rule is effective and kept.
 
623
 
 
624
    namespaces
 
625
        a dictionary {prefix: namespaceURI} containing the effective namespaces
 
626
        only. These are the latest set in the CSSStyleSheet.
 
627
    parentStyleSheet
 
628
        the parent CSSStyleSheet
 
629
    """
 
630
    def __init__(self, parentStyleSheet, log=None, *args):
 
631
        "no initial values are set, only the relevant sheet is"
 
632
        self.parentStyleSheet = parentStyleSheet
 
633
        self._log = log
 
634
 
 
635
    def __contains__(self, prefix):
 
636
        return prefix in self.namespaces
 
637
 
 
638
    def __delitem__(self, prefix):
 
639
        """deletes CSSNamespaceRule(s) with rule.prefix == prefix
 
640
 
 
641
        prefix '' and None are handled the same
 
642
        """
 
643
        if not prefix:
 
644
            prefix = u''
 
645
        delrule = self.__findrule(prefix)
 
646
        for i, rule in enumerate(ifilter(lambda r: r.type == r.NAMESPACE_RULE,
 
647
                            self.parentStyleSheet.cssRules)):
 
648
            if rule == delrule:
 
649
                self.parentStyleSheet.deleteRule(i)
 
650
                return
 
651
 
 
652
        self._log.error('Prefix %r not found.' % prefix,
 
653
                        error=xml.dom.NamespaceErr)
 
654
 
 
655
    def __getitem__(self, prefix):
 
656
        try:
 
657
            return self.namespaces[prefix]
 
658
        except KeyError, e:
 
659
            self._log.error('Prefix %r not found.' % prefix,
 
660
                            error=xml.dom.NamespaceErr)
 
661
 
 
662
    def __iter__(self):
 
663
        return self.namespaces.__iter__()
 
664
 
 
665
    def __len__(self):
 
666
        return len(self.namespaces)
 
667
 
 
668
    def __setitem__(self, prefix, namespaceURI):
 
669
        "replaces prefix or sets new rule, may raise NoModificationAllowedErr"
 
670
        if not prefix:
 
671
            prefix = u'' # None or ''
 
672
        rule = self.__findrule(prefix)
 
673
        if not rule:
 
674
            self.parentStyleSheet.insertRule(cssutils.css.CSSNamespaceRule(
 
675
                                                    prefix=prefix,
 
676
                                                    namespaceURI=namespaceURI),
 
677
                                  inOrder=True)
 
678
        else:
 
679
            if prefix in self.namespaces:
 
680
                rule.namespaceURI = namespaceURI # raises NoModificationAllowedErr
 
681
            if namespaceURI in self.namespaces.values():
 
682
                rule.prefix = prefix
 
683
 
 
684
    def __findrule(self, prefix):
 
685
        # returns namespace rule where prefix == key
 
686
        for rule in ifilter(lambda r: r.type == r.NAMESPACE_RULE,
 
687
                            reversed(self.parentStyleSheet.cssRules)):
 
688
            if rule.prefix == prefix:
 
689
                return rule
 
690
 
 
691
    def __getNamespaces(self):
 
692
        namespaces = {}
 
693
        for rule in ifilter(lambda r: r.type == r.NAMESPACE_RULE,
 
694
                            reversed(self.parentStyleSheet.cssRules)):
 
695
            if rule.namespaceURI not in namespaces.values():
 
696
                namespaces[rule.prefix] = rule.namespaceURI
 
697
        return namespaces
 
698
 
 
699
    namespaces = property(__getNamespaces,
 
700
        doc=u'Holds only effective @namespace rules in self.parentStyleSheets'
 
701
             '@namespace rules.')
 
702
 
 
703
    def get(self, prefix, default):
 
704
        return self.namespaces.get(prefix, default)
 
705
 
 
706
    def items(self):
 
707
        return self.namespaces.items()
 
708
 
 
709
    def keys(self):
 
710
        return self.namespaces.keys()
 
711
 
 
712
    def values(self):
 
713
        return self.namespaces.values()
 
714
 
 
715
    def prefixForNamespaceURI(self, namespaceURI):
 
716
        """
 
717
        returns effective prefix for given namespaceURI or raises IndexError
 
718
        if this cannot be found"""
 
719
        for prefix, uri in self.namespaces.items():
 
720
            if uri == namespaceURI:
 
721
                return prefix
 
722
        raise IndexError(u'NamespaceURI %r not found.' % namespaceURI)
 
723
 
 
724
    def __str__(self):
 
725
        return u"<cssutils.util.%s object parentStyleSheet=%r at 0x%x>" % (
 
726
                self.__class__.__name__, str(self.parentStyleSheet), id(self))
 
727
 
 
728
 
 
729
class _SimpleNamespaces(_Namespaces):
 
730
    """
 
731
    namespaces used in objects like Selector as long as they are not connected
 
732
    to a CSSStyleSheet
 
733
    """
 
734
    def __init__(self, log=None, *args):
 
735
        """init"""
 
736
        super(_SimpleNamespaces, self).__init__(parentStyleSheet=None, log=log)
 
737
        self.__namespaces = dict(*args)
 
738
 
 
739
    def __setitem__(self, prefix, namespaceURI):
 
740
        self.__namespaces[prefix] = namespaceURI
 
741
 
 
742
    namespaces = property(lambda self: self.__namespaces,
 
743
                          doc=u'Dict Wrapper for self.sheets @namespace rules.')
 
744
 
 
745
    def __str__(self):
 
746
        return u"<cssutils.util.%s object namespaces=%r at 0x%x>" % (
 
747
                self.__class__.__name__, self.namespaces, id(self))
 
748
 
 
749
    def __repr__(self):
 
750
        return u"cssutils.util.%s(%r)" % (self.__class__.__name__,
 
751
            self.namespaces)
 
752
 
 
753
 
 
754
def _defaultFetcher(url):
 
755
    """Retrieve data from ``url``. cssutils default implementation of fetch
 
756
    URL function.
 
757
 
 
758
    Returns ``(encoding, string)`` or ``None``
 
759
    """
 
760
    try:
 
761
        res = urllib2.urlopen(url)
 
762
    except OSError, e:
 
763
        # e.g if file URL and not found
 
764
        cssutils.log.warn(e, error=OSError)
 
765
    except (OSError, ValueError), e:
 
766
        # invalid url, e.g. "1"
 
767
        cssutils.log.warn(u'ValueError, %s' % e.message, error=ValueError)
 
768
    except urllib2.HTTPError, e:
 
769
        # http error, e.g. 404, e can be raised
 
770
        cssutils.log.warn(u'HTTPError opening url=%r: %s %s' %
 
771
                          (url, e.code, e.msg), error=e)
 
772
    except urllib2.URLError, e:
 
773
        # URLError like mailto: or other IO errors, e can be raised
 
774
        cssutils.log.warn(u'URLError, %s' % e.reason, error=e)
 
775
    else:
 
776
        if res:
 
777
            mimeType, encoding = encutils.getHTTPInfo(res)
 
778
            if mimeType != u'text/css':
 
779
                cssutils.log.error(u'Expected "text/css" mime type for url=%r but found: %r' %
 
780
                                  (url, mimeType), error=ValueError)
 
781
            return encoding, res.read()
 
782
 
 
783
def _readUrl(url, fetcher=None, overrideEncoding=None, parentEncoding=None):
 
784
    """
 
785
    Read cssText from url and decode it using all relevant methods (HTTP
 
786
    header, BOM, @charset). Returns
 
787
 
 
788
    - encoding used to decode text (which is needed to set encoding of
 
789
      stylesheet properly)
 
790
    - type of encoding (how it was retrieved, see list below)
 
791
    - decodedCssText
 
792
 
 
793
    ``fetcher``
 
794
        see cssutils.registerFetchUrl for details
 
795
    ``overrideEncoding``
 
796
        If given this encoding is used and all other encoding information is
 
797
        ignored (HTTP, BOM etc)
 
798
    ``parentEncoding``
 
799
        Encoding of parent stylesheet (while e.g. reading @import references sheets)
 
800
        or document if available.
 
801
 
 
802
    Priority or encoding information
 
803
    --------------------------------
 
804
    **cssutils only**: 0. overrideEncoding
 
805
 
 
806
    1. An HTTP "charset" parameter in a "Content-Type" field (or similar parameters in other protocols)
 
807
    2. BOM and/or @charset (see below)
 
808
    3. <link charset=""> or other metadata from the linking mechanism (if any)
 
809
    4. charset of referring style sheet or document (if any)
 
810
    5. Assume UTF-8
 
811
 
 
812
    """
 
813
    enctype = None
 
814
 
 
815
    if not fetcher:
 
816
        fetcher = _defaultFetcher
 
817
    r = fetcher(url)
 
818
    if r and len(r) == 2 and r[1] is not None:
 
819
        httpEncoding, content = r
 
820
 
 
821
        if overrideEncoding:
 
822
            enctype = 0 # 0. override encoding
 
823
            encoding = overrideEncoding
 
824
        elif httpEncoding:
 
825
            enctype = 1 # 1. HTTP
 
826
            encoding = httpEncoding
 
827
        else:
 
828
            # check content
 
829
            contentEncoding, explicit = cssutils.codec.detectencoding_str(content)
 
830
            if explicit:
 
831
                enctype = 2 # 2. BOM/@charset: explicitly
 
832
                encoding = contentEncoding
 
833
            elif parentEncoding:
 
834
                enctype = 4 # 4. parent stylesheet or document
 
835
                # may also be None in which case 5. is used in next step anyway
 
836
                encoding = parentEncoding
 
837
            else:
 
838
                enctype = 5 # 5. assume UTF-8
 
839
                encoding = 'utf-8'
 
840
 
 
841
        try:
 
842
            # encoding may still be wrong if encoding *is lying*!
 
843
            if content is not None:
 
844
                decodedCssText = codecs.lookup("css")[1](content, encoding=encoding)[0]
 
845
            else:
 
846
                decodedCssText = None
 
847
        except UnicodeDecodeError, e:
 
848
            cssutils.log.warn(e, neverraise=True)
 
849
            decodedCssText = None
 
850
 
 
851
        return encoding, enctype, decodedCssText
 
852
    else:
 
853
        return None, None, None