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