1"""MediaList implements DOM Level 2 Style Sheets MediaList. 2 3TODO: 4 - delete: maybe if deleting from all, replace *all* with all others? 5 - is unknown media an exception? 6""" 7from __future__ import (absolute_import, division, print_function, 8 unicode_literals) 9 10import xml.dom 11 12import css_parser 13from css_parser.helper import normalize, pushtoken 14from css_parser.prodparser import PreDef, Prod, ProdParser, Sequence 15 16from .mediaquery import MediaQuery 17 18__all__ = ['MediaList'] 19__docformat__ = 'restructuredtext' 20__version__ = '$Id$' 21 22 23# class MediaList(css_parser.util.Base, css_parser.util.ListSeq): 24 25 26class MediaList(css_parser.util._NewListBase): 27 """Provides the abstraction of an ordered collection of media, 28 without defining or constraining how this collection is 29 implemented. 30 31 A single media in the list is an instance of :class:`MediaQuery`. 32 An empty list is the same as a list that contains the medium "all". 33 34 New format with :class:`MediaQuery`:: 35 36 : S* [media_query [ ',' S* media_query ]* ]? 37 38 39 """ 40 41 def __init__(self, mediaText=None, parentRule=None, readonly=False): 42 """ 43 :param mediaText: 44 Unicodestring of parsable comma separared media 45 or a (Python) list of media. 46 :param parentRule: 47 CSSRule this medialist is used in, e.g. an @import or @media. 48 :param readonly: 49 Not used yet. 50 """ 51 super(MediaList, self).__init__() 52 self._wellformed = False 53 54 if isinstance(mediaText, list): 55 mediaText = ','.join(mediaText) 56 57 self._parentRule = parentRule 58 59 if mediaText: 60 self.mediaText = mediaText 61 62 self._readonly = readonly 63 64 def __repr__(self): 65 return "css_parser.stylesheets.%s(mediaText=%r)" % (self.__class__.__name__, self.mediaText) 66 67 def __str__(self): 68 return "<css_parser.stylesheets.%s object mediaText=%r at 0x%x>" % ( 69 self.__class__.__name__, self.mediaText, id(self)) 70 71 def __iter__(self): 72 for item in self._seq: 73 if item.type == 'MediaQuery': 74 yield item 75 76 length = property(lambda self: len(list(self)), 77 doc="The number of media in the list (DOM readonly).") 78 79 def _getMediaText(self): 80 return css_parser.ser.do_stylesheets_medialist(self) 81 82 def _setMediaText(self, mediaText): 83 """ 84 :param mediaText: 85 simple value or comma-separated list of media 86 87 :exceptions: 88 - - :exc:`~xml.dom.SyntaxErr`: 89 Raised if the specified string value has a syntax error and is 90 unparsable. 91 - - :exc:`~xml.dom.NoModificationAllowedErr`: 92 Raised if this media list is readonly. 93 """ 94 self._checkReadonly() 95 96 def mediaquery(): return Prod(name='MediaQueryStart', 97 match=lambda t, v: t == 'IDENT' or v == '(', 98 toSeq=lambda t, tokens: ('MediaQuery', 99 MediaQuery(pushtoken(t, tokens), 100 _partof=True)) 101 ) 102 prods = Sequence(Sequence(PreDef.comment(parent=self), 103 minmax=lambda: (0, None) 104 ), 105 mediaquery(), 106 Sequence(PreDef.comma(toSeq=False), 107 mediaquery(), 108 minmax=lambda: (0, None)) 109 110 ) 111 # parse 112 ok, seq, store, unused = ProdParser().parse(mediaText, 113 'MediaList', 114 prods, debug="ml") 115 116 # each mq must be valid 117 atleastone = False 118 119 for item in seq: 120 v = item.value 121 if isinstance(v, MediaQuery): 122 if not v.wellformed: 123 ok = False 124 break 125 else: 126 atleastone = True 127 128 # must be at least one value! 129 if not atleastone: 130 ok = False 131 self._wellformed = ok 132 self._log.error('MediaQuery: No content.', 133 error=xml.dom.SyntaxErr) 134 135 self._wellformed = ok 136 137 if ok: 138 mediaTypes = [] 139 finalseq = css_parser.util.Seq(readonly=False) 140 commentseqonly = css_parser.util.Seq(readonly=False) 141 for item in seq: 142 # filter for doubles? 143 if item.type == 'MediaQuery': 144 mediaType = item.value.mediaType 145 if mediaType: 146 if mediaType == 'all': 147 # remove anthing else and keep all+comments(!) only 148 finalseq = commentseqonly 149 finalseq.append(item) 150 break 151 elif mediaType in mediaTypes: 152 continue 153 else: 154 mediaTypes.append(mediaType) 155 elif isinstance(item.value, css_parser.css.csscomment.CSSComment): 156 commentseqonly.append(item) 157 158 finalseq.append(item) 159 160 self._setSeq(finalseq) 161 162 mediaText = property(_getMediaText, _setMediaText, 163 doc="The parsable textual representation of the media list.") 164 165 def __prepareset(self, newMedium): 166 # used by appendSelector and __setitem__ 167 self._checkReadonly() 168 169 if not isinstance(newMedium, MediaQuery): 170 newMedium = MediaQuery(newMedium) 171 172 if newMedium.wellformed: 173 return newMedium 174 175 def __setitem__(self, index, newMedium): 176 """Overwriting ListSeq.__setitem__ 177 178 Any duplicate items are **not yet** removed. 179 """ 180 # TODO: remove duplicates? 181 newMedium = self.__prepareset(newMedium) 182 if newMedium: 183 self._seq[index] = (newMedium, 'MediaQuery', None, None) 184 185 def appendMedium(self, newMedium): 186 """Add the `newMedium` to the end of the list. 187 If the `newMedium` is already used, it is first removed. 188 189 :param newMedium: 190 a string or a :class:`~css_parser.stylesheets.MediaQuery` 191 :returns: Wellformedness of `newMedium`. 192 :exceptions: 193 - :exc:`~xml.dom.InvalidCharacterErr`: 194 If the medium contains characters that are invalid in the 195 underlying style language. 196 - :exc:`~xml.dom.InvalidModificationErr`: 197 If mediaText is "all" and a new medium is tried to be added. 198 Exception is "handheld" which is set in any case (Opera does handle 199 "all, handheld" special, this special case might be removed in the 200 future). 201 - :exc:`~xml.dom.NoModificationAllowedErr`: 202 Raised if this list is readonly. 203 """ 204 newMedium = self.__prepareset(newMedium) 205 206 if newMedium: 207 mts = [normalize(item.value.mediaType) for item in self] 208 newmt = normalize(newMedium.mediaType) 209 210 self._seq._readonly = False 211 212 if 'all' in mts: 213 self._log.info( 214 'MediaList: Ignoring new medium %r as already specified "all" (set ``mediaText`` instead).' % 215 newMedium, error=xml.dom.InvalidModificationErr) 216 217 elif newmt and newmt in mts: 218 # might be empty 219 self.deleteMedium(newmt) 220 self._seq.append(newMedium, 'MediaQuery') 221 222 else: 223 if 'all' == newmt: 224 self._clearSeq() 225 226 self._seq.append(newMedium, 'MediaQuery') 227 228 self._seq._readonly = True 229 230 return True 231 232 else: 233 return False 234 235 def append(self, newMedium): 236 "Same as :meth:`appendMedium`." 237 self.appendMedium(newMedium) 238 239 def deleteMedium(self, oldMedium): 240 """Delete a medium from the list. 241 242 :param oldMedium: 243 delete this medium from the list. 244 :exceptions: 245 - :exc:`~xml.dom.NotFoundErr`: 246 Raised if `oldMedium` is not in the list. 247 - :exc:`~xml.dom.NoModificationAllowedErr`: 248 Raised if this list is readonly. 249 """ 250 self._checkReadonly() 251 oldMedium = normalize(oldMedium) 252 253 for i, mq in enumerate(self): 254 if normalize(mq.value.mediaType) == oldMedium: 255 del self[i] 256 break 257 else: 258 self._log.error('"%s" not in this MediaList' % oldMedium, 259 error=xml.dom.NotFoundErr) 260 261 def item(self, index): 262 """Return the mediaType of the `index`'th element in the list. 263 If `index` is greater than or equal to the number of media in the 264 list, returns ``None``. 265 """ 266 try: 267 return self[index].mediaType 268 except IndexError: 269 return None 270 271 parentRule = property(lambda self: self._parentRule, 272 doc="The CSSRule (e.g. an @media or @import rule " 273 "this list is part of or None") 274 275 wellformed = property(lambda self: self._wellformed) 276