Package cssutils :: Package css :: Module cssstylesheet |
[hide private]
[frames] | no frames]

# Source Code for Module cssutils.css.cssstylesheet

      1  """
      2  CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet.
      3
      4  Partly also:
      5      - http://dev.w3.org/csswg/cssom/#the-cssstylesheet
      6      - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/
      7
      8  TODO:
      9      - ownerRule and ownerNode
     10  """
     11  __all__ = ['CSSStyleSheet']
     12  __docformat__ = 'restructuredtext'
     13  __version__ = '$Id: cssstylesheet.py 1192 2008-03-21 22:35:04Z cthedot $'
     14
     15  import xml.dom
     16  import cssutils.stylesheets
     17  from cssutils.util import _Namespaces, _SimpleNamespaces, Deprecated
     18
     19 -class CSSStyleSheet(cssutils.stylesheets.StyleSheet):
     20      """
     21      The CSSStyleSheet interface represents a CSS style sheet.
     22
     23      Properties
     24      ==========
     25      CSSOM
     26      -----
     27      cssRules
     28          of type CSSRuleList, (DOM readonly)
     29      encoding
     30          reflects the encoding of an @charset rule or 'utf-8' (default)
     31          if set to ``None``
     32      ownerRule
     33          of type CSSRule, readonly. If this sheet is imported this is a ref
     34          to the @import rule that imports it.
     35
     36      Inherits properties from stylesheet.StyleSheet
     37
     38      cssutils
     39      --------
     40      cssText: string
     41          a textual representation of the stylesheet
     42      namespaces
     43          reflects set @namespace rules of this rule.
     44          A dict of {prefix: namespaceURI} mapping.
     45
     46      Format
     47      ======
     48      stylesheet
     49        : [ CHARSET_SYM S* STRING S* ';' ]?
     50          [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
     51          [ namespace [S|CDO|CDC]* ]* # according to @namespace WD
     52          [ [ ruleset | media | page ] [S|CDO|CDC]* ]*
     53      """
     54 -    def __init__(self, href=None, media=None, title=u'', disabled=None,
     55                   ownerNode=None, parentStyleSheet=None, readonly=False,
     56                   ownerRule=None):
     57          """
     58          init parameters are the same as for stylesheets.StyleSheet
     59          """
     60          super(CSSStyleSheet, self).__init__(
     61                  'text/css', href, media, title, disabled,
     62                  ownerNode, parentStyleSheet)
     63
     64          self._ownerRule = ownerRule
     65          self.cssRules = cssutils.css.CSSRuleList()
     66          self.cssRules.append = self.insertRule
     67          self.cssRules.extend = self.insertRule
     68          self._namespaces = _Namespaces(parentStyleSheet=self)
     69          self._readonly = readonly
     70
     71 -    def __iter__(self):
     72          "generator which iterates over cssRules."
     73          for rule in self.cssRules:
     74              yield rule
     75
     76 -    def _cleanNamespaces(self):
     77          "removes all namespace rules with same namespaceURI but last one set"
     78          rules = self.cssRules
     79          namespaceitems = self.namespaces.items()
     80          i = 0
     81          while i < len(rules):
     82              rule = rules[i]
     83              if rule.type == rule.NAMESPACE_RULE and \
     84                 (rule.prefix, rule.namespaceURI) not in namespaceitems:
     85                  self.deleteRule(i)
     86              else:
     87                  i += 1
     88
     89 -    def _getUsedURIs(self):
     90          "returns set of URIs used in the sheet"
     91          useduris = set()
     92          for r1 in self:
     93              if r1.STYLE_RULE == r1.type:
     94                  useduris.update(r1.selectorList._getUsedUris())
     95              elif r1.MEDIA_RULE == r1.type:
     96                  for r2 in r1:
     97                      if r2.type == r2.STYLE_RULE:
     98                          useduris.update(r2.selectorList._getUsedUris())
     99          return useduris
    100
    101 -    def _getCssText(self):
    102          return cssutils.ser.do_CSSStyleSheet(self)
    103
    104 -    def _setCssText(self, cssText):
    105          """
    106          (cssutils)
    107          Parses ``cssText`` and overwrites the whole stylesheet.
    108
    109          :param cssText:
    110              a parseable string or a tuple of (cssText, dict-of-namespaces)
    111          :Exceptions:
    112              - `NAMESPACE_ERR`:
    113                If a namespace prefix is found which is not declared.
    114              - `NO_MODIFICATION_ALLOWED_ERR`: (self)
    115                Raised if the rule is readonly.
    116              - `SYNTAX_ERR`:
    117                Raised if the specified CSS string value has a syntax error and
    118                is unparsable.
    119          """
    120          self._checkReadonly()
    121
    122          cssText, namespaces = self._splitNamespacesOff(cssText)
    123          if not namespaces:
    124              namespaces = _SimpleNamespaces()
    125
    126          tokenizer = self._tokenize2(cssText)
    127          newseq = [] #cssutils.css.CSSRuleList()
    128
    129          # for closures: must be a mutable
    130          new = { 'namespaces': namespaces}
    131
    132          def S(expected, seq, token, tokenizer=None):
    133              # @charset must be at absolute beginning of style sheet
    134              if expected == 0:
    135                  return 1
    136              else:
    137                  return expected
    138
    139          def COMMENT(expected, seq, token, tokenizer=None):
    140              "special: sets parent*"
    141              comment = cssutils.css.CSSComment([token],
    142                                  parentStyleSheet=self.parentStyleSheet)
    143              seq.append(comment)
    144              return expected
    145
    146          def charsetrule(expected, seq, token, tokenizer):
    147              rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self)
    148              rule.cssText = self._tokensupto2(tokenizer, token)
    149              if expected > 0 or len(seq) > 0:
    150                  self._log.error(
    151                      u'CSSStylesheet: CSSCharsetRule only allowed at beginning of stylesheet.',
    152                      token, xml.dom.HierarchyRequestErr)
    153              else:
    154                  if rule.wellformed:
    155                      seq.append(rule)
    156              return 1
    157
    158          def importrule(expected, seq, token, tokenizer):
    159              rule = cssutils.css.CSSImportRule(parentStyleSheet=self)
    160              rule.cssText = self._tokensupto2(tokenizer, token)
    161              if expected > 1:
    162                  self._log.error(
    163                      u'CSSStylesheet: CSSImportRule not allowed here.',
    164                      token, xml.dom.HierarchyRequestErr)
    165              else:
    166                  if rule.wellformed:
    167                      seq.append(rule)
    168              return 1
    169
    170          def namespacerule(expected, seq, token, tokenizer):
    171              rule = cssutils.css.CSSNamespaceRule(
    172                                  cssText=self._tokensupto2(tokenizer, token),
    173                                  parentStyleSheet=self)
    174              if expected > 2:
    175                  self._log.error(
    176                      u'CSSStylesheet: CSSNamespaceRule not allowed here.',
    177                      token, xml.dom.HierarchyRequestErr)
    178              else:
    179                  if rule.wellformed:
    180                      seq.append(rule)
    181                      # temporary namespaces given to CSSStyleRule and @media
    182                      new['namespaces'][rule.prefix] = rule.namespaceURI
    183              return 2
    184
    185          def fontfacerule(expected, seq, token, tokenizer):
    186              rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self)
    187              rule.cssText = self._tokensupto2(tokenizer, token)
    188              if rule.wellformed:
    189                  seq.append(rule)
    190              return 3
    191
    192          def mediarule(expected, seq, token, tokenizer):
    193              rule = cssutils.css.CSSMediaRule()
    194              rule.cssText = (self._tokensupto2(tokenizer, token),
    195                              new['namespaces'])
    196              if rule.wellformed:
    197                  rule._parentStyleSheet=self
    198                  for r in rule:
    199                      r._parentStyleSheet=self
    200                  seq.append(rule)
    201              return 3
    202
    203          def pagerule(expected, seq, token, tokenizer):
    204              rule = cssutils.css.CSSPageRule(parentStyleSheet=self)
    205              rule.cssText = self._tokensupto2(tokenizer, token)
    206              if rule.wellformed:
    207                  seq.append(rule)
    208              return 3
    209
    210          def unknownrule(expected, seq, token, tokenizer):
    211              rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self)
    212              rule.cssText = self._tokensupto2(tokenizer, token)
    213              if rule.wellformed:
    214                  seq.append(rule)
    215              return expected
    216
    217          def ruleset(expected, seq, token, tokenizer):
    218              rule = cssutils.css.CSSStyleRule()
    219              rule.cssText = (self._tokensupto2(tokenizer, token),
    220                              new['namespaces'])
    221              if rule.wellformed:
    222                  rule._parentStyleSheet=self
    223                  seq.append(rule)
    224              return 3
    225
    226          # expected:
    227          # ['CHARSET', 'IMPORT', 'NAMESPACE', ('PAGE', 'MEDIA', ruleset)]
    228          wellformed, expected = self._parse(0, newseq, tokenizer,
    229              {'S': S,
    230               'COMMENT': COMMENT,
    231               'CDO': lambda *ignored: None,
    232               'CDC': lambda *ignored: None,
    233               'CHARSET_SYM': charsetrule,
    234               'FONT_FACE_SYM': fontfacerule,
    235               'IMPORT_SYM': importrule,
    236               'NAMESPACE_SYM': namespacerule,
    237               'PAGE_SYM': pagerule,
    238               'MEDIA_SYM': mediarule,
    239               'ATKEYWORD': unknownrule
    240               },
    241               default=ruleset)
    242
    243          if wellformed:
    244              del self.cssRules[:]
    245              for rule in newseq:
    246                  self.insertRule(rule, _clean=False)
    247              self._cleanNamespaces()
    248
    249      cssText = property(_getCssText, _setCssText,
    250              "(cssutils) a textual representation of the stylesheet")
    251
    252 -    def _setEncoding(self, encoding):
    253          """
    254          sets encoding of charset rule if present or inserts new charsetrule
    255          with given encoding. If encoding if None removes charsetrule if
    256          present.
    257          """
    258          try:
    259              rule = self.cssRules[0]
    260          except IndexError:
    261              rule = None
    262          if rule and rule.CHARSET_RULE == rule.type:
    263              if encoding:
    264                  rule.encoding = encoding
    265              else:
    266                  self.deleteRule(0)
    267          elif encoding:
    268              self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0)
    269
    270 -    def _getEncoding(self):
    271          "return encoding if @charset rule if given or default of 'utf-8'"
    272          try:
    273              return self.cssRules[0].encoding
    274          except (IndexError, AttributeError):
    275              return 'utf-8'
    276
    277      encoding = property(_getEncoding, _setEncoding,
    278              "(cssutils) reflects the encoding of an @charset rule or 'UTF-8' (default) if set to ``None``")
    279
    280      namespaces = property(lambda self: self._namespaces,
    281                            doc="Namespaces used in this CSSStyleSheet.")
    282
    283 -    def add(self, rule):
    284          """
    285          Adds rule to stylesheet at appropriate position.
    286          Same as ``sheet.insertRule(rule, inOrder=True)``.
    287          """
    288          return self.insertRule(rule, index=None, inOrder=True)
    289
    290 -    def deleteRule(self, index):
    291          """
    292          Used to delete a rule from the style sheet.
    293
    294          :param index:
    295              of the rule to remove in the StyleSheet's rule list. For an
    296              index < 0 **no** INDEX_SIZE_ERR is raised but rules for
    297              normal Python lists are used. E.g. ``deleteRule(-1)`` removes
    298              the last rule in cssRules.
    299          :Exceptions:
    300              - `INDEX_SIZE_ERR`: (self)
    301                Raised if the specified index does not correspond to a rule in
    302                the style sheet's rule list.
    303              - `NAMESPACE_ERR`: (self)
    304                Raised if removing this rule would result in an invalid StyleSheet
    305              - `NO_MODIFICATION_ALLOWED_ERR`: (self)
    306                Raised if this style sheet is readonly.
    307          """
    308          self._checkReadonly()
    309
    310          try:
    311              rule = self.cssRules[index]
    312          except IndexError:
    313              raise xml.dom.IndexSizeErr(
    314                  u'CSSStyleSheet: %s is not a valid index in the rulelist of length %i' % (
    315                  index, self.cssRules.length))
    316          else:
    317              if rule.type == rule.NAMESPACE_RULE:
    318                  # check all namespacerules if used
    319                  uris = [r.namespaceURI for r in self if r.type == r.NAMESPACE_RULE]
    320                  useduris = self._getUsedURIs()
    321                  if rule.namespaceURI in useduris and\
    322                     uris.count(rule.namespaceURI) == 1:
    323                      raise xml.dom.NoModificationAllowedErr(
    324                          u'CSSStyleSheet: NamespaceURI defined in this rule is used, cannot remove.')
    325                      return
    326
    327              rule._parentStyleSheet = None # detach
    328              del self.cssRules[index] # delete from StyleSheet
    329
    330 -    def insertRule(self, rule, index=None, inOrder=False, _clean=True):
    331          """
    332          Used to insert a new rule into the style sheet. The new rule now
    333          becomes part of the cascade.
    334
    335          :Parameters:
    336              rule
    337                  a parsable DOMString, in cssutils also a CSSRule or a
    338                  CSSRuleList
    339              index
    340                  of the rule before the new rule will be inserted.
    341                  If the specified index is equal to the length of the
    342                  StyleSheet's rule collection, the rule will be added to the end
    343                  of the style sheet.
    344                  If index is not given or None rule will be appended to rule
    345                  list.
    346              inOrder
    347                  if True the rule will be put to a proper location while
    348                  ignoring index but without raising HIERARCHY_REQUEST_ERR.
    349                  The resulting index is returned nevertheless
    350          :returns: the index within the stylesheet's rule collection
    351          :Exceptions:
    352              - `HIERARCHY_REQUEST_ERR`: (self)
    353                Raised if the rule cannot be inserted at the specified index
    354                e.g. if an @import rule is inserted after a standard rule set
    355                or other at-rule.
    356              - `INDEX_SIZE_ERR`: (self)
    357                Raised if the specified index is not a valid insertion point.
    358              - `NO_MODIFICATION_ALLOWED_ERR`: (self)
    359                Raised if this style sheet is readonly.
    360              - `SYNTAX_ERR`: (rule)
    361                Raised if the specified rule has a syntax error and is
    362                unparsable.
    363          """
    364          self._checkReadonly()
    365
    366          # check position
    367          if index is None:
    368              index = len(self.cssRules)
    369          elif index < 0 or index > self.cssRules.length:
    370              raise xml.dom.IndexSizeErr(
    371                  u'CSSStyleSheet: Invalid index %s for CSSRuleList with a length of %s.' % (
    372                      index, self.cssRules.length))
    373              return
    374
    375          if isinstance(rule, basestring):
    376              # parse a new rule
    377              tempsheet = CSSStyleSheet()
    378              tempsheet.cssText = (rule, self._namespaces)
    379              if len(tempsheet.cssRules) != 1 or (tempsheet.cssRules and
    380               not isinstance(tempsheet.cssRules[0], cssutils.css.CSSRule)):
    381                  self._log.error(u'CSSStyleSheet: Invalid Rule: %s' % rule)
    382                  return
    383              rule = tempsheet.cssRules[0]
    384              rule._parentStyleSheet = None
    385
    386          elif isinstance(rule, cssutils.css.CSSRuleList):
    387              # insert all rules
    388              for i, r in enumerate(rule):
    389                  self.insertRule(r, index + i)
    390              return index
    391
    392          if not rule.wellformed:
    393              self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.')
    394              return
    395
    396          # CHECK HIERARCHY
    397          # @charset
    398          if rule.type == rule.CHARSET_RULE:
    399              if inOrder:
    400                  index = 0
    401                  # always first and only
    402                  if (self.cssRules and self.cssRules[0].type == rule.CHARSET_RULE):
    403                      self.cssRules[0].encoding = rule.encoding
    404                  else:
    405                      self.cssRules.insert(0, rule)
    406              elif index != 0 or (self.cssRules and
    407                                self.cssRules[0].type == rule.CHARSET_RULE):
    408                  self._log.error(
    409                      u'CSSStylesheet: @charset only allowed once at the beginning of a stylesheet.',
    410                      error=xml.dom.HierarchyRequestErr)
    411                  return
    412              else:
    413                  self.cssRules.insert(index, rule)
    414
    415          # @unknown or comment
    416          elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder:
    417              if index == 0 and self.cssRules and\
    418                 self.cssRules[0].type == rule.CHARSET_RULE:
    419                  self._log.error(
    420                      u'CSSStylesheet: @charset must be the first rule.',
    421                      error=xml.dom.HierarchyRequestErr)
    422                  return
    423              else:
    424                  self.cssRules.insert(index, rule)
    425
    426          # @import
    427          elif rule.type == rule.IMPORT_RULE:
    428              if inOrder:
    429                  # automatic order
    430                  if rule.type in (r.type for r in self):
    431                      # find last of this type
    432                      for i, r in enumerate(reversed(self.cssRules)):
    433                          if r.type == rule.type:
    434                              index = len(self.cssRules) - i
    435                              break
    436                  else:
    437                      # find first point to insert
    438                      if self.cssRules and self.cssRules[0].type in (rule.CHARSET_RULE,
    439                                                                     rule.COMMENT):
    440                          index = 1
    441                      else:
    442                          index = 0
    443              else:
    444                  # after @charset
    445                  if index == 0 and self.cssRules and\
    446                     self.cssRules[0].type == rule.CHARSET_RULE:
    447                      self._log.error(
    448                          u'CSSStylesheet: Found @charset at index 0.',
    449                          error=xml.dom.HierarchyRequestErr)
    450                      return
    451                  # before @namespace, @page, @font-face, @media and stylerule
    452                  for r in self.cssRules[:index]:
    453                      if r.type in (r.NAMESPACE_RULE, r.MEDIA_RULE, r.PAGE_RULE,
    454                                    r.STYLE_RULE, r.FONT_FACE_RULE):
    455                          self._log.error(
    456                              u'CSSStylesheet: Cannot insert @import here, found @namespace, @media, @page or CSSStyleRule before index %s.' %
    457                              index,
    458                              error=xml.dom.HierarchyRequestErr)
    459                          return
    460              self.cssRules.insert(index, rule)
    461
    462          # @namespace
    463          elif rule.type == rule.NAMESPACE_RULE:
    464              if inOrder:
    465                  if rule.type in (r.type for r in self):
    466                      # find last of this type
    467                      for i, r in enumerate(reversed(self.cssRules)):
    468                          if r.type == rule.type:
    469                              index = len(self.cssRules) - i
    470                              break
    471                  else:
    472                      # find first point to insert
    473                      for i, r in enumerate(self.cssRules):
    474                          if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE,
    475                                        r.FONT_FACE_RULE):
    476                              index = i # before these
    477                              break
    478              else:
    479                  # after @charset and @import
    480                  for r in self.cssRules[index:]:
    481                      if r.type in (r.CHARSET_RULE, r.IMPORT_RULE):
    482                          self._log.error(
    483                              u'CSSStylesheet: Cannot insert @namespace here, found @charset or @import after index %s.' %
    484                              index,
    485                              error=xml.dom.HierarchyRequestErr)
    486                          return
    487                  # before @media and stylerule
    488                  for r in self.cssRules[:index]:
    489                      if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE,
    490                                    r.FONT_FACE_RULE):
    491                          self._log.error(
    492                              u'CSSStylesheet: Cannot insert @namespace here, found @media, @page or CSSStyleRule before index %s.' %
    493                              index,
    494                              error=xml.dom.HierarchyRequestErr)
    495                          return
    496
    497              if not (rule.prefix in self.namespaces and
    498                 self.namespaces[rule.prefix] == rule.namespaceURI):
    499                  # no doublettes
    500                  self.cssRules.insert(index, rule)
    501                  if _clean:
    502                      self._cleanNamespaces()
    503
    504          # all other
    505          else:
    506              if inOrder:
    507                  # after last of this kind or at end of sheet
    508                  if rule.type in (r.type for r in self):
    509                      # find last of this type
    510                      for i, r in enumerate(reversed(self.cssRules)):
    511                          if r.type == rule.type:
    512                              index = len(self.cssRules) - i
    513                              break
    514                      self.cssRules.insert(index, rule)
    515                  else:
    516                      self.cssRules.append(rule) # to end as no same present
    517              else:
    518                  for r in self.cssRules[index:]:
    519                      if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE):
    520                          self._log.error(
    521                              u'CSSStylesheet: Cannot insert rule here, found @charset, @import or @namespace before index %s.' %
    522                              index,
    523                              error=xml.dom.HierarchyRequestErr)
    524                          return
    525                  self.cssRules.insert(index, rule)
    526
    527          rule._parentStyleSheet = self
    528          if rule.MEDIA_RULE == rule.type:
    529              for r in rule:
    530                  r._parentStyleSheet = self
    531          return index
    532
    533      ownerRule = property(lambda self: self._ownerRule,
    534                           doc="(DOM attribute) NOT IMPLEMENTED YET")
    535
    536      @Deprecated('Use cssutils.replaceUrls(sheet, replacer) instead.')
    537 -    def replaceUrls(self, replacer):
    538          """
    539          **EXPERIMENTAL**
    540
    541          Utility method to replace all ``url(urlstring)`` values in
    542          ``CSSImportRules`` and ``CSSStyleDeclaration`` objects (properties).
    543
    544          ``replacer`` must be a function which is called with a single
    545          argument ``urlstring`` which is the current value of url()
    546          excluding ``url(`` and ``)``. It still may have surrounding
    547          single or double quotes though.
    548          """
    549          cssutils.replaceUrls(self, replacer)
    550
    551 -    def setSerializer(self, cssserializer):
    552          """
    553          Sets the global Serializer used for output of all stylesheet
    554          output.
    555          """
    556          if isinstance(cssserializer, cssutils.CSSSerializer):
    557              cssutils.ser = cssserializer
    558          else:
    559              raise ValueError(u'Serializer must be an instance of cssutils.CSSSerializer.')
    560
    561 -    def setSerializerPref(self, pref, value):
    562          """
    563          Sets Preference of CSSSerializer used for output of this
    564          stylesheet. See cssutils.serialize.Preferences for possible
    565          preferences to be set.
    566          """
    567          cssutils.ser.prefs.__setattr__(pref, value)
    568
    569 -    def __repr__(self):
    570          if self.media:
    571              mediaText = self.media.mediaText
    572          else:
    573              mediaText = None
    574          return "cssutils.css.%s(href=%r, media=%r, title=%r)" % (
    575                  self.__class__.__name__,
    576                  self.href, mediaText, self.title)
    577
    578 -    def __str__(self):
    579          if self.media:
    580              mediaText = self.media.mediaText
    581          else:
    582              mediaText = None
    583          return "<cssutils.css.%s object encoding=%r href=%r "\
    584                 "media=%r title=%r namespaces=%r at 0x%x>" % (
    585                  self.__class__.__name__, self.encoding, self.href,
    586                  mediaText, self.title, self.namespaces.namespaces,
    587                  id(self))
    588