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