Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tinycss2/parser.py: 80%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from itertools import chain
3from .ast import AtRule, Declaration, ParseError, QualifiedRule
4from .tokenizer import parse_component_value_list
7def _to_token_iterator(input, skip_comments=False):
8 """Iterate component values out of string or component values iterable.
10 :type input: :obj:`str` or :term:`iterable`
11 :param input: A string or an iterable of :term:`component values`.
12 :type skip_comments: :obj:`bool`
13 :param skip_comments: If the input is a string, ignore all CSS comments.
14 :returns: An iterator yielding :term:`component values`.
16 """
17 if isinstance(input, str):
18 input = parse_component_value_list(input, skip_comments)
19 return iter(input)
22def _next_significant(tokens):
23 """Return the next significant (neither whitespace or comment) token.
25 :type tokens: :term:`iterator`
26 :param tokens: An iterator yielding :term:`component values`.
27 :returns: A :term:`component value`, or :obj:`None`.
29 """
30 for token in tokens:
31 if token.type not in ('whitespace', 'comment'):
32 return token
35def parse_one_component_value(input, skip_comments=False):
36 """Parse a single :diagram:`component value`.
38 This is used e.g. for an attribute value
39 referred to by ``attr(foo length)``.
41 :type input: :obj:`str` or :term:`iterable`
42 :param input: A string or an iterable of :term:`component values`.
43 :type skip_comments: :obj:`bool`
44 :param skip_comments: If the input is a string, ignore all CSS comments.
45 :returns:
46 A :term:`component value` (that is neither whitespace or comment),
47 or a :class:`~tinycss2.ast.ParseError`.
49 """
50 tokens = _to_token_iterator(input, skip_comments)
51 first = _next_significant(tokens)
52 second = _next_significant(tokens)
53 if first is None:
54 return ParseError(1, 1, 'empty', 'Input is empty')
55 if second is not None:
56 return ParseError(
57 second.source_line, second.source_column, 'extra-input',
58 'Got more than one token')
59 else:
60 return first
63def parse_one_declaration(input, skip_comments=False):
64 """Parse a single :diagram:`declaration`.
66 This is used e.g. for a declaration in an `@supports
67 <https://drafts.csswg.org/css-conditional/#at-supports>`_ test.
69 :type input: :obj:`str` or :term:`iterable`
70 :param input: A string or an iterable of :term:`component values`.
71 :type skip_comments: :obj:`bool`
72 :param skip_comments: If the input is a string, ignore all CSS comments.
73 :returns:
74 A :class:`~tinycss2.ast.Declaration`
75 or :class:`~tinycss2.ast.ParseError`.
77 Any whitespace or comment before the ``:`` colon is dropped.
79 """
80 tokens = _to_token_iterator(input, skip_comments)
81 first_token = _next_significant(tokens)
82 if first_token is None:
83 return ParseError(1, 1, 'empty', 'Input is empty')
84 return _parse_declaration(first_token, tokens)
87def _consume_remnants(input, nested):
88 for token in input:
89 if token == ';':
90 return
91 elif nested and token == '}':
92 return
95def _parse_declaration(first_token, tokens, nested=True):
96 """Parse a declaration.
98 Consume :obj:`tokens` until the end of the declaration or the first error.
100 :type first_token: :term:`component value`
101 :param first_token: The first component value of the rule.
102 :type tokens: :term:`iterator`
103 :param tokens: An iterator yielding :term:`component values`.
104 :type nested: :obj:`bool`
105 :param nested: Whether the declaration is nested or top-level.
106 :returns:
107 A :class:`~tinycss2.ast.Declaration`
108 or :class:`~tinycss2.ast.ParseError`.
110 """
111 name = first_token
112 if name.type != 'ident':
113 _consume_remnants(tokens, nested)
114 return ParseError(
115 name.source_line, name.source_column, 'invalid',
116 f'Expected <ident> for declaration name, got {name.type}.')
118 colon = _next_significant(tokens)
119 if colon is None:
120 _consume_remnants(tokens, nested)
121 return ParseError(
122 name.source_line, name.source_column, 'invalid',
123 "Expected ':' after declaration name, got EOF")
124 elif colon != ':':
125 _consume_remnants(tokens, nested)
126 return ParseError(
127 colon.source_line, colon.source_column, 'invalid',
128 "Expected ':' after declaration name, got {colon.type}.")
130 value = []
131 state = 'value'
132 contains_non_whitespace = False
133 contains_simple_block = False
134 for i, token in enumerate(tokens):
135 if state == 'value' and token == '!':
136 state = 'bang'
137 bang_position = i
138 elif (state == 'bang' and token.type == 'ident'
139 and token.lower_value == 'important'):
140 state = 'important'
141 elif token.type not in ('whitespace', 'comment'):
142 state = 'value'
143 if token.type == '{} block':
144 if contains_non_whitespace:
145 contains_simple_block = True
146 else:
147 contains_non_whitespace = True
148 else:
149 contains_non_whitespace = True
150 value.append(token)
152 if state == 'important':
153 del value[bang_position:]
155 # TODO: Handle custom property names
157 if contains_simple_block and contains_non_whitespace:
158 return ParseError(
159 colon.source_line, colon.source_column, 'invalid',
160 'Declaration contains {} block')
162 # TODO: Handle unicode-range
164 return Declaration(
165 name.source_line, name.source_column, name.value, name.lower_value,
166 value, state == 'important')
169def _consume_blocks_content(first_token, tokens):
170 """Consume declaration or nested rule."""
171 declaration_tokens = []
172 semicolon_token = []
173 if first_token != ';' and first_token.type != '{} block':
174 for token in tokens:
175 if token == ';':
176 semicolon_token.append(token)
177 break
178 declaration_tokens.append(token)
179 if token.type == '{} block':
180 break
181 declaration = _parse_declaration(
182 first_token, iter(declaration_tokens), nested=True)
183 if declaration.type == 'declaration':
184 return declaration
185 else:
186 tokens = chain(declaration_tokens, semicolon_token, tokens)
187 return _consume_qualified_rule(first_token, tokens, stop_token=';', nested=True)
190def _consume_declaration_in_list(first_token, tokens):
191 """Like :func:`_parse_declaration`, but stop at the first ``;``.
193 Deprecated, use :func:`_consume_blocks_content` instead.
195 """
196 other_declaration_tokens = []
197 for token in tokens:
198 if token == ';':
199 break
200 other_declaration_tokens.append(token)
201 return _parse_declaration(first_token, iter(other_declaration_tokens))
204def parse_blocks_contents(input, skip_comments=False, skip_whitespace=False):
205 """Parse a block’s contents.
207 This is used e.g. for the :attr:`~tinycss2.ast.QualifiedRule.content`
208 of a style rule or ``@page`` rule, or for the ``style`` attribute of an
209 HTML element.
211 In contexts that don’t expect any at-rule and/or qualified rule,
212 all :class:`~tinycss2.ast.AtRule` and/or
213 :class:`~tinycss2.ast.QualifiedRule` objects should simply be rejected as
214 invalid.
216 :type input: :obj:`str` or :term:`iterable`
217 :param input: A string or an iterable of :term:`component values`.
218 :type skip_comments: :obj:`bool`
219 :param skip_comments:
220 Ignore CSS comments at the top-level of the list.
221 If the input is a string, ignore all comments.
222 :type skip_whitespace: :obj:`bool`
223 :param skip_whitespace:
224 Ignore whitespace at the top-level of the list.
225 Whitespace is still preserved
226 in the :attr:`~tinycss2.ast.Declaration.value` of declarations
227 and the :attr:`~tinycss2.ast.AtRule.prelude`
228 and :attr:`~tinycss2.ast.AtRule.content` of at-rules.
229 :returns:
230 A list of
231 :class:`~tinycss2.ast.Declaration`,
232 :class:`~tinycss2.ast.AtRule`,
233 :class:`~tinycss2.ast.QualifiedRule`,
234 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
235 :class:`~tinycss2.ast.WhitespaceToken`
236 (if ``skip_whitespace`` is false),
237 and :class:`~tinycss2.ast.ParseError` objects
239 """
240 tokens = _to_token_iterator(input, skip_comments)
241 result = []
242 for token in tokens:
243 if token.type == 'whitespace':
244 if not skip_whitespace:
245 result.append(token)
246 elif token.type == 'comment':
247 if not skip_comments:
248 result.append(token)
249 elif token.type == 'at-keyword':
250 result.append(_consume_at_rule(token, tokens))
251 elif token != ';':
252 result.append(_consume_blocks_content(token, tokens))
253 return result
256def parse_declaration_list(input, skip_comments=False, skip_whitespace=False):
257 """Parse a :diagram:`declaration list` (which may also contain at-rules).
259 Deprecated and removed from CSS Syntax Level 3. Use
260 :func:`parse_blocks_contents` instead.
262 This is used e.g. for the :attr:`~tinycss2.ast.QualifiedRule.content`
263 of a style rule or ``@page`` rule, or for the ``style`` attribute of an
264 HTML element.
266 In contexts that don’t expect any at-rule, all
267 :class:`~tinycss2.ast.AtRule` objects should simply be rejected as invalid.
269 :type input: :obj:`str` or :term:`iterable`
270 :param input: A string or an iterable of :term:`component values`.
271 :type skip_comments: :obj:`bool`
272 :param skip_comments:
273 Ignore CSS comments at the top-level of the list.
274 If the input is a string, ignore all comments.
275 :type skip_whitespace: :obj:`bool`
276 :param skip_whitespace:
277 Ignore whitespace at the top-level of the list.
278 Whitespace is still preserved
279 in the :attr:`~tinycss2.ast.Declaration.value` of declarations
280 and the :attr:`~tinycss2.ast.AtRule.prelude`
281 and :attr:`~tinycss2.ast.AtRule.content` of at-rules.
282 :returns:
283 A list of
284 :class:`~tinycss2.ast.Declaration`,
285 :class:`~tinycss2.ast.AtRule`,
286 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
287 :class:`~tinycss2.ast.WhitespaceToken`
288 (if ``skip_whitespace`` is false),
289 and :class:`~tinycss2.ast.ParseError` objects
291 """
292 tokens = _to_token_iterator(input, skip_comments)
293 result = []
294 for token in tokens:
295 if token.type == 'whitespace':
296 if not skip_whitespace:
297 result.append(token)
298 elif token.type == 'comment':
299 if not skip_comments:
300 result.append(token)
301 elif token.type == 'at-keyword':
302 result.append(_consume_at_rule(token, tokens))
303 elif token != ';':
304 result.append(_consume_declaration_in_list(token, tokens))
305 return result
308def parse_one_rule(input, skip_comments=False):
309 """Parse a single :diagram:`qualified rule` or :diagram:`at-rule`.
311 This would be used e.g. by `insertRule()
312 <https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule>`_
313 in an implementation of CSSOM.
315 :type input: :obj:`str` or :term:`iterable`
316 :param input: A string or an iterable of :term:`component values`.
317 :type skip_comments: :obj:`bool`
318 :param skip_comments:
319 If the input is a string, ignore all CSS comments.
320 :returns:
321 A :class:`~tinycss2.ast.QualifiedRule`,
322 :class:`~tinycss2.ast.AtRule`,
323 or :class:`~tinycss2.ast.ParseError` objects.
325 Any whitespace or comment before or after the rule is dropped.
327 """
328 tokens = _to_token_iterator(input, skip_comments)
329 first = _next_significant(tokens)
330 if first is None:
331 return ParseError(1, 1, 'empty', 'Input is empty')
333 rule = _consume_rule(first, tokens)
334 next = _next_significant(tokens)
335 if next is not None:
336 return ParseError(
337 next.source_line, next.source_column, 'extra-input',
338 'Expected a single rule, got %s after the first rule.' % next.type)
339 return rule
342def parse_rule_list(input, skip_comments=False, skip_whitespace=False):
343 """Parse a non-top-level :diagram:`rule list`.
345 Deprecated and removed from CSS Syntax. Use :func:`parse_blocks_contents`
346 instead.
348 This is used for parsing the :attr:`~tinycss2.ast.AtRule.content`
349 of nested rules like ``@media``.
350 This differs from :func:`parse_stylesheet` in that
351 top-level ``<!--`` and ``-->`` tokens are not ignored.
353 :type input: :obj:`str` or :term:`iterable`
354 :param input: A string or an iterable of :term:`component values`.
355 :type skip_comments: :obj:`bool`
356 :param skip_comments:
357 Ignore CSS comments at the top-level of the list.
358 If the input is a string, ignore all comments.
359 :type skip_whitespace: :obj:`bool`
360 :param skip_whitespace:
361 Ignore whitespace at the top-level of the list.
362 Whitespace is still preserved
363 in the :attr:`~tinycss2.ast.QualifiedRule.prelude`
364 and the :attr:`~tinycss2.ast.QualifiedRule.content` of rules.
365 :returns:
366 A list of
367 :class:`~tinycss2.ast.QualifiedRule`,
368 :class:`~tinycss2.ast.AtRule`,
369 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
370 :class:`~tinycss2.ast.WhitespaceToken`
371 (if ``skip_whitespace`` is false),
372 and :class:`~tinycss2.ast.ParseError` objects.
374 """
375 tokens = _to_token_iterator(input, skip_comments)
376 result = []
377 for token in tokens:
378 if token.type == 'whitespace':
379 if not skip_whitespace:
380 result.append(token)
381 elif token.type == 'comment':
382 if not skip_comments:
383 result.append(token)
384 else:
385 result.append(_consume_rule(token, tokens))
386 return result
389def parse_stylesheet(input, skip_comments=False, skip_whitespace=False):
390 """Parse :diagram:`stylesheet` from text.
392 This is used e.g. for a ``<style>`` HTML element.
394 This differs from :func:`parse_rule_list` in that
395 top-level ``<!--`` and ``-->`` tokens are ignored.
396 This is a legacy quirk for the ``<style>`` HTML element.
398 :type input: :obj:`str` or :term:`iterable`
399 :param input: A string or an iterable of :term:`component values`.
400 :type skip_comments: :obj:`bool`
401 :param skip_comments:
402 Ignore CSS comments at the top-level of the stylesheet.
403 If the input is a string, ignore all comments.
404 :type skip_whitespace: :obj:`bool`
405 :param skip_whitespace:
406 Ignore whitespace at the top-level of the stylesheet.
407 Whitespace is still preserved
408 in the :attr:`~tinycss2.ast.QualifiedRule.prelude`
409 and the :attr:`~tinycss2.ast.QualifiedRule.content` of rules.
410 :returns:
411 A list of
412 :class:`~tinycss2.ast.QualifiedRule`,
413 :class:`~tinycss2.ast.AtRule`,
414 :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
415 :class:`~tinycss2.ast.WhitespaceToken`
416 (if ``skip_whitespace`` is false),
417 and :class:`~tinycss2.ast.ParseError` objects.
419 """
420 tokens = _to_token_iterator(input, skip_comments)
421 result = []
422 for token in tokens:
423 if token.type == 'whitespace':
424 if not skip_whitespace:
425 result.append(token)
426 elif token.type == 'comment':
427 if not skip_comments:
428 result.append(token)
429 elif token not in ('<!--', '-->'):
430 result.append(_consume_rule(token, tokens))
431 return result
434def _consume_rule(first_token, tokens):
435 """Parse a qualified rule or at-rule.
437 Consume just enough of :obj:`tokens` for this rule.
439 :type first_token: :term:`component value`
440 :param first_token: The first component value of the rule.
441 :type tokens: :term:`iterator`
442 :param tokens: An iterator yielding :term:`component values`.
443 :returns:
444 A :class:`~tinycss2.ast.QualifiedRule`,
445 :class:`~tinycss2.ast.AtRule`,
446 or :class:`~tinycss2.ast.ParseError`.
448 """
449 if first_token.type == 'at-keyword':
450 return _consume_at_rule(first_token, tokens)
451 return _consume_qualified_rule(first_token, tokens)
454def _consume_at_rule(at_keyword, tokens):
455 """Parse an at-rule.
457 Consume just enough of :obj:`tokens` for this rule.
459 :type at_keyword: :class:`AtKeywordToken`
460 :param at_keyword: The at-rule keyword token starting this rule.
461 :type tokens: :term:`iterator`
462 :param tokens: An iterator yielding :term:`component values`.
463 :type nested: :obj:`bool`
464 :param nested: Whether the at-rule is nested or top-level.
465 :returns:
466 A :class:`~tinycss2.ast.QualifiedRule`,
467 or :class:`~tinycss2.ast.ParseError`.
469 """
470 prelude = []
471 content = None
472 for token in tokens:
473 if token.type == '{} block':
474 # TODO: handle nested at-rules
475 # https://drafts.csswg.org/css-syntax-3/#consume-at-rule
476 content = token.content
477 break
478 elif token == ';':
479 break
480 prelude.append(token)
481 return AtRule(
482 at_keyword.source_line, at_keyword.source_column, at_keyword.value,
483 at_keyword.lower_value, prelude, content)
486def _rule_error(token, name):
487 """Create rule parse error raised because of given token."""
488 return ParseError(
489 token.source_line, token.source_column, 'invalid',
490 f'{name} reached before {{}} block for a qualified rule.')
493def _consume_qualified_rule(first_token, tokens, nested=False,
494 stop_token=None):
495 """Consume a qualified rule.
497 Consume just enough of :obj:`tokens` for this rule.
499 :type first_token: :term:`component value`
500 :param first_token: The first component value of the rule.
501 :type tokens: :term:`iterator`
502 :param tokens: An iterator yielding :term:`component values`.
503 :type nested: :obj:`bool`
504 :param nested: Whether the rule is nested or top-level.
505 :type stop_token: :class:`~tinycss2.ast.Node`
506 :param stop_token: A token that ends rule parsing when met.
508 """
509 if first_token == stop_token:
510 return _rule_error(first_token, 'Stop token')
511 if first_token.type == '{} block':
512 prelude = []
513 block = first_token
514 else:
515 prelude = [first_token]
516 for token in tokens:
517 if token == stop_token:
518 return _rule_error(token, 'Stop token')
519 if token.type == '{} block':
520 block = token
521 # TODO: handle special case for CSS variables (using "nested")
522 # https://drafts.csswg.org/css-syntax-3/#consume-qualified-rule
523 break
524 prelude.append(token)
525 else:
526 return _rule_error(prelude[-1], 'EOF')
527 return QualifiedRule(
528 first_token.source_line, first_token.source_column, prelude, block.content)