1"""CSSMediaRule implements DOM Level 2 CSS CSSMediaRule."""
2from __future__ import unicode_literals, division, absolute_import, print_function
3
4__all__ = ['CSSMediaRule']
5__docformat__ = 'restructuredtext'
6__version__ = '$Id$'
7
8from . import cssrule
9import css_parser
10import xml.dom
11
12import sys
13if sys.version_info[0] >= 3:
14    string_type = str
15else:
16    string_type = basestring
17
18
19class CSSMediaRule(cssrule.CSSRuleRules):
20    """
21    Objects implementing the CSSMediaRule interface can be identified by the
22    MEDIA_RULE constant. On these objects the type attribute must return the
23    value of that constant.
24
25    Format::
26
27      : MEDIA_SYM S* medium [ COMMA S* medium ]*
28
29          STRING? # the name
30
31      LBRACE S* ruleset* '}' S*;
32
33    ``cssRules``
34        All Rules in this media rule, a :class:`~css_parser.css.CSSRuleList`.
35    """
36
37    def __init__(self, mediaText='all', name=None,
38                 parentRule=None, parentStyleSheet=None, readonly=False):
39        """constructor"""
40        super(CSSMediaRule, self).__init__(parentRule=parentRule,
41                                           parentStyleSheet=parentStyleSheet)
42        self._atkeyword = '@media'
43
44        # 1. media
45        if mediaText:
46            self.media = mediaText
47        else:
48            self.media = css_parser.stylesheets.MediaList()
49
50        self.name = name
51        self._readonly = readonly
52
53    def __repr__(self):
54        return "css_parser.css.%s(mediaText=%r)" % (
55            self.__class__.__name__,
56            self.media.mediaText)
57
58    def __str__(self):
59        return "<css_parser.css.%s object mediaText=%r at 0x%x>" % (
60            self.__class__.__name__,
61            self.media.mediaText,
62            id(self))
63
64    def _getCssText(self):
65        """Return serialized property cssText."""
66        return css_parser.ser.do_CSSMediaRule(self)
67
68    def _setCssText(self, cssText):
69        """
70        :param cssText:
71            a parseable string or a tuple of (cssText, dict-of-namespaces)
72        :Exceptions:
73            - :exc:`~xml.dom.NamespaceErr`:
74              Raised if a specified selector uses an unknown namespace
75              prefix.
76            - :exc:`~xml.dom.SyntaxErr`:
77              Raised if the specified CSS string value has a syntax error and
78              is unparsable.
79            - :exc:`~xml.dom.InvalidModificationErr`:
80              Raised if the specified CSS string value represents a different
81              type of rule than the current one.
82            - :exc:`~xml.dom.HierarchyRequestErr`:
83              Raised if the rule cannot be inserted at this point in the
84              style sheet.
85            - :exc:`~xml.dom.NoModificationAllowedErr`:
86              Raised if the rule is readonly.
87        """
88        # media "name"? { cssRules }
89        super(CSSMediaRule, self)._setCssText(cssText)
90
91        # might be (cssText, namespaces)
92        cssText, namespaces = self._splitNamespacesOff(cssText)
93
94        tokenizer = self._tokenize2(cssText)
95        attoken = self._nexttoken(tokenizer, None)
96        if self._type(attoken) != self._prods.MEDIA_SYM:
97            self._log.error('CSSMediaRule: No CSSMediaRule found: %s' %
98                            self._valuestr(cssText),
99                            error=xml.dom.InvalidModificationErr)
100
101        else:
102            # save if parse goes wrong
103            oldMedia = self._media
104            oldCssRules = self._cssRules
105
106            ok = True
107
108            # media
109            mediatokens, end = self._tokensupto2(tokenizer,
110                                                 mediaqueryendonly=True,
111                                                 separateEnd=True)
112            if '{' == self._tokenvalue(end)\
113               or self._prods.STRING == self._type(end):
114                self.media = css_parser.stylesheets.MediaList(parentRule=self)
115                # TODO: remove special case
116                self.media.mediaText = mediatokens
117                ok = ok and self.media.wellformed
118            else:
119                ok = False
120
121            # name (optional)
122            name = None
123            nameseq = self._tempSeq()
124            if self._prods.STRING == self._type(end):
125                name = self._stringtokenvalue(end)
126                # TODO: for now comments are lost after name
127                nametokens, end = self._tokensupto2(tokenizer,
128                                                    blockstartonly=True,
129                                                    separateEnd=True)
130                wellformed, expected = self._parse(None,
131                                                   nameseq,
132                                                   nametokens,
133                                                   {})
134                if not wellformed:
135                    ok = False
136                    self._log.error('CSSMediaRule: Syntax Error: %s' %
137                                    self._valuestr(cssText))
138
139            # check for {
140            if '{' != self._tokenvalue(end):
141                self._log.error('CSSMediaRule: No "{" found: %s' %
142                                self._valuestr(cssText))
143                return
144
145            # cssRules
146            cssrulestokens, braceOrEOF = self._tokensupto2(tokenizer,
147                                                           mediaendonly=True,
148                                                           separateEnd=True)
149            nonetoken = self._nexttoken(tokenizer, None)
150            if 'EOF' == self._type(braceOrEOF):
151                # HACK!!!
152                # TODO: Not complete, add EOF to rule and } to @media
153                cssrulestokens.append(braceOrEOF)
154                braceOrEOF = ('CHAR', '}', 0, 0)
155                self._log.debug('CSSMediaRule: Incomplete, adding "}".',
156                                token=braceOrEOF, neverraise=True)
157
158            if '}' != self._tokenvalue(braceOrEOF):
159                self._log.error('CSSMediaRule: No "}" found.',
160                                token=braceOrEOF)
161            elif nonetoken:
162                self._log.error('CSSMediaRule: Trailing content found.',
163                                token=nonetoken)
164            else:
165                # for closures: must be a mutable
166                new = {'wellformed': True}
167
168                def COMMENT(expected, seq, token, tokenizer=None):
169                    self.insertRule(css_parser.css.CSSComment(
170                        [token],
171                        parentRule=self,
172                        parentStyleSheet=self.parentStyleSheet))
173                    return expected
174
175                def ruleset(expected, seq, token, tokenizer):
176                    rule = css_parser.css.CSSStyleRule(
177                            parentRule=self,
178                            parentStyleSheet=self.parentStyleSheet)
179                    rule.cssText = self._tokensupto2(tokenizer, token)
180                    if rule.wellformed:
181                        self.insertRule(rule)
182                    return expected
183
184                def atrule(expected, seq, token, tokenizer):
185                    # TODO: get complete rule!
186                    tokens = self._tokensupto2(tokenizer, token)
187                    atval = self._tokenvalue(token)
188                    factories = {
189                        '@page': css_parser.css.CSSPageRule,
190                        '@media': CSSMediaRule,
191                    }
192                    if atval in ('@charset ', '@font-face', '@import',
193                                 '@namespace', '@variables'):
194                        self._log.error('CSSMediaRule: This rule is not '
195                                        'allowed in CSSMediaRule - ignored: '
196                                        '%s.' % self._valuestr(tokens),
197                                        token=token,
198                                        error=xml.dom.HierarchyRequestErr)
199                    elif atval in factories:
200                        rule = factories[atval](
201                            parentRule=self,
202                            parentStyleSheet=self.parentStyleSheet)
203                        rule.cssText = tokens
204                        if rule.wellformed:
205                            self.insertRule(rule)
206                    else:
207                        rule = css_parser.css.CSSUnknownRule(
208                                tokens,
209                                parentRule=self,
210                                parentStyleSheet=self.parentStyleSheet)
211                        if rule.wellformed:
212                            self.insertRule(rule)
213                    return expected
214
215                # save for possible reset
216                oldCssRules = self.cssRules
217
218                self.cssRules = css_parser.css.CSSRuleList()
219                seq = []  # not used really
220
221                tokenizer = iter(cssrulestokens)
222                wellformed, expected = self._parse(braceOrEOF,
223                                                   seq,
224                                                   tokenizer, {
225                                                       'COMMENT': COMMENT,
226                                                       'CHARSET_SYM': atrule,
227                                                       'FONT_FACE_SYM': atrule,
228                                                       'IMPORT_SYM': atrule,
229                                                       'NAMESPACE_SYM': atrule,
230                                                       'PAGE_SYM': atrule,
231                                                       'MEDIA_SYM': atrule,
232                                                       'ATKEYWORD': atrule
233                                                   },
234                                                   default=ruleset,
235                                                   new=new)
236                ok = ok and wellformed
237
238            if ok:
239                self.name = name
240                self._setSeq(nameseq)
241            else:
242                self._media = oldMedia
243                self._cssRules = oldCssRules
244
245    cssText = property(_getCssText, _setCssText,
246                       doc="(DOM) The parsable textual representation of this "
247                           "rule.")
248
249    def _setName(self, name):
250        # Under Python 2.x this was basestring but given unicode literals ...
251        if isinstance(name, string_type) or name is None:
252            # "" or ''
253            if not name:
254                name = None
255
256            self._name = name
257        else:
258            self._log.error('CSSImportRule: Not a valid name: %s' % name)
259
260    name = property(lambda self: self._name, _setName,
261                    doc="An optional name for this media rule.")
262
263    def _setMedia(self, media):
264        """
265        :param media:
266            a :class:`~css_parser.stylesheets.MediaList` or string
267        """
268        self._checkReadonly()
269
270        # Under Python 2.x this was basestring but given unicode literals ...
271        if isinstance(media, string_type):
272            self._media = css_parser.stylesheets.MediaList(
273                    mediaText=media, parentRule=self)
274        else:
275            media._parentRule = self
276            self._media = media
277
278        # NOT IN @media seq at all?!
279#        # update seq
280#        for i, item in enumerate(self.seq):
281#            if item.type == 'media':
282#                self._seq[i] = (self._media, 'media', None, None)
283#                break
284#        else:
285#            # insert after @media if not in seq at all
286#            self.seq.insert(0,
287#                             self._media, 'media', None, None)
288
289    media = property(lambda self: self._media, _setMedia,
290                     doc="(DOM) A list of media types for this rule "
291                         "of type :class:`~css_parser.stylesheets.MediaList`.")
292
293    def insertRule(self, rule, index=None):
294        """Implements base ``insertRule``."""
295        rule, index = self._prepareInsertRule(rule, index)
296
297        if rule is False or rule is True:
298            # done or error
299            return
300
301        # check hierarchy
302        if isinstance(rule, css_parser.css.CSSCharsetRule) or \
303           isinstance(rule, css_parser.css.CSSFontFaceRule) or \
304           isinstance(rule, css_parser.css.CSSImportRule) or \
305           isinstance(rule, css_parser.css.CSSNamespaceRule) or \
306           isinstance(rule, css_parser.css.MarginRule):
307            self._log.error('%s: This type of rule is not allowed here: %s'
308                            % (self.__class__.__name__, rule.cssText),
309                            error=xml.dom.HierarchyRequestErr)
310            return
311
312        return self._finishInsertRule(rule, index)
313
314    type = property(lambda self: self.MEDIA_RULE,
315                    doc="The type of this rule, as defined by a CSSRule "
316                        "type constant.")
317
318    wellformed = property(lambda self: self.media.wellformed)
319