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