| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- from itertools import chain
- from .ast import AtRule, Declaration, ParseError, QualifiedRule
- from .tokenizer import parse_component_value_list
- def _to_token_iterator(input, skip_comments=False):
- """Iterate component values out of string or component values iterable.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments: If the input is a string, ignore all CSS comments.
- :returns: An iterator yielding :term:`component values`.
- """
- if isinstance(input, str):
- input = parse_component_value_list(input, skip_comments)
- return iter(input)
- def _next_significant(tokens):
- """Return the next significant (neither whitespace or comment) token.
- :type tokens: :term:`iterator`
- :param tokens: An iterator yielding :term:`component values`.
- :returns: A :term:`component value`, or :obj:`None`.
- """
- for token in tokens:
- if token.type not in ('whitespace', 'comment'):
- return token
- def parse_one_component_value(input, skip_comments=False):
- """Parse a single :diagram:`component value`.
- This is used e.g. for an attribute value
- referred to by ``attr(foo length)``.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments: If the input is a string, ignore all CSS comments.
- :returns:
- A :term:`component value` (that is neither whitespace or comment),
- or a :class:`~tinycss2.ast.ParseError`.
- """
- tokens = _to_token_iterator(input, skip_comments)
- first = _next_significant(tokens)
- second = _next_significant(tokens)
- if first is None:
- return ParseError(1, 1, 'empty', 'Input is empty')
- if second is not None:
- return ParseError(
- second.source_line, second.source_column, 'extra-input',
- 'Got more than one token')
- else:
- return first
- def parse_one_declaration(input, skip_comments=False):
- """Parse a single :diagram:`declaration`.
- This is used e.g. for a declaration in an `@supports
- <https://drafts.csswg.org/css-conditional/#at-supports>`_ test.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments: If the input is a string, ignore all CSS comments.
- :returns:
- A :class:`~tinycss2.ast.Declaration`
- or :class:`~tinycss2.ast.ParseError`.
- Any whitespace or comment before the ``:`` colon is dropped.
- """
- tokens = _to_token_iterator(input, skip_comments)
- first_token = _next_significant(tokens)
- if first_token is None:
- return ParseError(1, 1, 'empty', 'Input is empty')
- return _parse_declaration(first_token, tokens)
- def _consume_remnants(input, nested):
- for token in input:
- if token == ';':
- return
- elif nested and token == '}':
- return
- def _parse_declaration(first_token, tokens, nested=True):
- """Parse a declaration.
- Consume :obj:`tokens` until the end of the declaration or the first error.
- :type first_token: :term:`component value`
- :param first_token: The first component value of the rule.
- :type tokens: :term:`iterator`
- :param tokens: An iterator yielding :term:`component values`.
- :type nested: :obj:`bool`
- :param nested: Whether the declaration is nested or top-level.
- :returns:
- A :class:`~tinycss2.ast.Declaration`
- or :class:`~tinycss2.ast.ParseError`.
- """
- name = first_token
- if name.type != 'ident':
- _consume_remnants(tokens, nested)
- return ParseError(
- name.source_line, name.source_column, 'invalid',
- f'Expected <ident> for declaration name, got {name.type}.')
- colon = _next_significant(tokens)
- if colon is None:
- _consume_remnants(tokens, nested)
- return ParseError(
- name.source_line, name.source_column, 'invalid',
- "Expected ':' after declaration name, got EOF")
- elif colon != ':':
- _consume_remnants(tokens, nested)
- return ParseError(
- colon.source_line, colon.source_column, 'invalid',
- "Expected ':' after declaration name, got {colon.type}.")
- value = []
- state = 'value'
- contains_non_whitespace = False
- contains_simple_block = False
- for i, token in enumerate(tokens):
- if state == 'value' and token == '!':
- state = 'bang'
- bang_position = i
- elif (state == 'bang' and token.type == 'ident'
- and token.lower_value == 'important'):
- state = 'important'
- elif token.type not in ('whitespace', 'comment'):
- state = 'value'
- if token.type == '{} block':
- if contains_non_whitespace:
- contains_simple_block = True
- else:
- contains_non_whitespace = True
- else:
- contains_non_whitespace = True
- value.append(token)
- if state == 'important':
- del value[bang_position:]
- # TODO: Handle custom property names
- if contains_simple_block and contains_non_whitespace:
- return ParseError(
- colon.source_line, colon.source_column, 'invalid',
- 'Declaration contains {} block')
- # TODO: Handle unicode-range
- return Declaration(
- name.source_line, name.source_column, name.value, name.lower_value,
- value, state == 'important')
- def _consume_blocks_content(first_token, tokens):
- """Consume declaration or nested rule."""
- declaration_tokens = []
- semicolon_token = []
- if first_token != ';' and first_token.type != '{} block':
- for token in tokens:
- if token == ';':
- semicolon_token.append(token)
- break
- declaration_tokens.append(token)
- if token.type == '{} block':
- break
- declaration = _parse_declaration(
- first_token, iter(declaration_tokens), nested=True)
- if declaration.type == 'declaration':
- return declaration
- else:
- tokens = chain(declaration_tokens, semicolon_token, tokens)
- return _consume_qualified_rule(first_token, tokens, stop_token=';', nested=True)
- def _consume_declaration_in_list(first_token, tokens):
- """Like :func:`_parse_declaration`, but stop at the first ``;``.
- Deprecated, use :func:`_consume_blocks_content` instead.
- """
- other_declaration_tokens = []
- for token in tokens:
- if token == ';':
- break
- other_declaration_tokens.append(token)
- return _parse_declaration(first_token, iter(other_declaration_tokens))
- def parse_blocks_contents(input, skip_comments=False, skip_whitespace=False):
- """Parse a block’s contents.
- This is used e.g. for the :attr:`~tinycss2.ast.QualifiedRule.content`
- of a style rule or ``@page`` rule, or for the ``style`` attribute of an
- HTML element.
- In contexts that don’t expect any at-rule and/or qualified rule,
- all :class:`~tinycss2.ast.AtRule` and/or
- :class:`~tinycss2.ast.QualifiedRule` objects should simply be rejected as
- invalid.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments:
- Ignore CSS comments at the top-level of the list.
- If the input is a string, ignore all comments.
- :type skip_whitespace: :obj:`bool`
- :param skip_whitespace:
- Ignore whitespace at the top-level of the list.
- Whitespace is still preserved
- in the :attr:`~tinycss2.ast.Declaration.value` of declarations
- and the :attr:`~tinycss2.ast.AtRule.prelude`
- and :attr:`~tinycss2.ast.AtRule.content` of at-rules.
- :returns:
- A list of
- :class:`~tinycss2.ast.Declaration`,
- :class:`~tinycss2.ast.AtRule`,
- :class:`~tinycss2.ast.QualifiedRule`,
- :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
- :class:`~tinycss2.ast.WhitespaceToken`
- (if ``skip_whitespace`` is false),
- and :class:`~tinycss2.ast.ParseError` objects
- """
- tokens = _to_token_iterator(input, skip_comments)
- result = []
- for token in tokens:
- if token.type == 'whitespace':
- if not skip_whitespace:
- result.append(token)
- elif token.type == 'comment':
- if not skip_comments:
- result.append(token)
- elif token.type == 'at-keyword':
- result.append(_consume_at_rule(token, tokens))
- elif token != ';':
- result.append(_consume_blocks_content(token, tokens))
- return result
- def parse_declaration_list(input, skip_comments=False, skip_whitespace=False):
- """Parse a :diagram:`declaration list` (which may also contain at-rules).
- Deprecated and removed from CSS Syntax Level 3. Use
- :func:`parse_blocks_contents` instead.
- This is used e.g. for the :attr:`~tinycss2.ast.QualifiedRule.content`
- of a style rule or ``@page`` rule, or for the ``style`` attribute of an
- HTML element.
- In contexts that don’t expect any at-rule, all
- :class:`~tinycss2.ast.AtRule` objects should simply be rejected as invalid.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments:
- Ignore CSS comments at the top-level of the list.
- If the input is a string, ignore all comments.
- :type skip_whitespace: :obj:`bool`
- :param skip_whitespace:
- Ignore whitespace at the top-level of the list.
- Whitespace is still preserved
- in the :attr:`~tinycss2.ast.Declaration.value` of declarations
- and the :attr:`~tinycss2.ast.AtRule.prelude`
- and :attr:`~tinycss2.ast.AtRule.content` of at-rules.
- :returns:
- A list of
- :class:`~tinycss2.ast.Declaration`,
- :class:`~tinycss2.ast.AtRule`,
- :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
- :class:`~tinycss2.ast.WhitespaceToken`
- (if ``skip_whitespace`` is false),
- and :class:`~tinycss2.ast.ParseError` objects
- """
- tokens = _to_token_iterator(input, skip_comments)
- result = []
- for token in tokens:
- if token.type == 'whitespace':
- if not skip_whitespace:
- result.append(token)
- elif token.type == 'comment':
- if not skip_comments:
- result.append(token)
- elif token.type == 'at-keyword':
- result.append(_consume_at_rule(token, tokens))
- elif token != ';':
- result.append(_consume_declaration_in_list(token, tokens))
- return result
- def parse_one_rule(input, skip_comments=False):
- """Parse a single :diagram:`qualified rule` or :diagram:`at-rule`.
- This would be used e.g. by `insertRule()
- <https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule>`_
- in an implementation of CSSOM.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments:
- If the input is a string, ignore all CSS comments.
- :returns:
- A :class:`~tinycss2.ast.QualifiedRule`,
- :class:`~tinycss2.ast.AtRule`,
- or :class:`~tinycss2.ast.ParseError` objects.
- Any whitespace or comment before or after the rule is dropped.
- """
- tokens = _to_token_iterator(input, skip_comments)
- first = _next_significant(tokens)
- if first is None:
- return ParseError(1, 1, 'empty', 'Input is empty')
- rule = _consume_rule(first, tokens)
- next = _next_significant(tokens)
- if next is not None:
- return ParseError(
- next.source_line, next.source_column, 'extra-input',
- 'Expected a single rule, got %s after the first rule.' % next.type)
- return rule
- def parse_rule_list(input, skip_comments=False, skip_whitespace=False):
- """Parse a non-top-level :diagram:`rule list`.
- Deprecated and removed from CSS Syntax. Use :func:`parse_blocks_contents`
- instead.
- This is used for parsing the :attr:`~tinycss2.ast.AtRule.content`
- of nested rules like ``@media``.
- This differs from :func:`parse_stylesheet` in that
- top-level ``<!--`` and ``-->`` tokens are not ignored.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments:
- Ignore CSS comments at the top-level of the list.
- If the input is a string, ignore all comments.
- :type skip_whitespace: :obj:`bool`
- :param skip_whitespace:
- Ignore whitespace at the top-level of the list.
- Whitespace is still preserved
- in the :attr:`~tinycss2.ast.QualifiedRule.prelude`
- and the :attr:`~tinycss2.ast.QualifiedRule.content` of rules.
- :returns:
- A list of
- :class:`~tinycss2.ast.QualifiedRule`,
- :class:`~tinycss2.ast.AtRule`,
- :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
- :class:`~tinycss2.ast.WhitespaceToken`
- (if ``skip_whitespace`` is false),
- and :class:`~tinycss2.ast.ParseError` objects.
- """
- tokens = _to_token_iterator(input, skip_comments)
- result = []
- for token in tokens:
- if token.type == 'whitespace':
- if not skip_whitespace:
- result.append(token)
- elif token.type == 'comment':
- if not skip_comments:
- result.append(token)
- else:
- result.append(_consume_rule(token, tokens))
- return result
- def parse_stylesheet(input, skip_comments=False, skip_whitespace=False):
- """Parse :diagram:`stylesheet` from text.
- This is used e.g. for a ``<style>`` HTML element.
- This differs from :func:`parse_rule_list` in that
- top-level ``<!--`` and ``-->`` tokens are ignored.
- This is a legacy quirk for the ``<style>`` HTML element.
- :type input: :obj:`str` or :term:`iterable`
- :param input: A string or an iterable of :term:`component values`.
- :type skip_comments: :obj:`bool`
- :param skip_comments:
- Ignore CSS comments at the top-level of the stylesheet.
- If the input is a string, ignore all comments.
- :type skip_whitespace: :obj:`bool`
- :param skip_whitespace:
- Ignore whitespace at the top-level of the stylesheet.
- Whitespace is still preserved
- in the :attr:`~tinycss2.ast.QualifiedRule.prelude`
- and the :attr:`~tinycss2.ast.QualifiedRule.content` of rules.
- :returns:
- A list of
- :class:`~tinycss2.ast.QualifiedRule`,
- :class:`~tinycss2.ast.AtRule`,
- :class:`~tinycss2.ast.Comment` (if ``skip_comments`` is false),
- :class:`~tinycss2.ast.WhitespaceToken`
- (if ``skip_whitespace`` is false),
- and :class:`~tinycss2.ast.ParseError` objects.
- """
- tokens = _to_token_iterator(input, skip_comments)
- result = []
- for token in tokens:
- if token.type == 'whitespace':
- if not skip_whitespace:
- result.append(token)
- elif token.type == 'comment':
- if not skip_comments:
- result.append(token)
- elif token not in ('<!--', '-->'):
- result.append(_consume_rule(token, tokens))
- return result
- def _consume_rule(first_token, tokens):
- """Parse a qualified rule or at-rule.
- Consume just enough of :obj:`tokens` for this rule.
- :type first_token: :term:`component value`
- :param first_token: The first component value of the rule.
- :type tokens: :term:`iterator`
- :param tokens: An iterator yielding :term:`component values`.
- :returns:
- A :class:`~tinycss2.ast.QualifiedRule`,
- :class:`~tinycss2.ast.AtRule`,
- or :class:`~tinycss2.ast.ParseError`.
- """
- if first_token.type == 'at-keyword':
- return _consume_at_rule(first_token, tokens)
- return _consume_qualified_rule(first_token, tokens)
- def _consume_at_rule(at_keyword, tokens):
- """Parse an at-rule.
- Consume just enough of :obj:`tokens` for this rule.
- :type at_keyword: :class:`AtKeywordToken`
- :param at_keyword: The at-rule keyword token starting this rule.
- :type tokens: :term:`iterator`
- :param tokens: An iterator yielding :term:`component values`.
- :type nested: :obj:`bool`
- :param nested: Whether the at-rule is nested or top-level.
- :returns:
- A :class:`~tinycss2.ast.QualifiedRule`,
- or :class:`~tinycss2.ast.ParseError`.
- """
- prelude = []
- content = None
- for token in tokens:
- if token.type == '{} block':
- # TODO: handle nested at-rules
- # https://drafts.csswg.org/css-syntax-3/#consume-at-rule
- content = token.content
- break
- elif token == ';':
- break
- prelude.append(token)
- return AtRule(
- at_keyword.source_line, at_keyword.source_column, at_keyword.value,
- at_keyword.lower_value, prelude, content)
- def _rule_error(token, name):
- """Create rule parse error raised because of given token."""
- return ParseError(
- token.source_line, token.source_column, 'invalid',
- f'{name} reached before {{}} block for a qualified rule.')
- def _consume_qualified_rule(first_token, tokens, nested=False,
- stop_token=None):
- """Consume a qualified rule.
- Consume just enough of :obj:`tokens` for this rule.
- :type first_token: :term:`component value`
- :param first_token: The first component value of the rule.
- :type tokens: :term:`iterator`
- :param tokens: An iterator yielding :term:`component values`.
- :type nested: :obj:`bool`
- :param nested: Whether the rule is nested or top-level.
- :type stop_token: :class:`~tinycss2.ast.Node`
- :param stop_token: A token that ends rule parsing when met.
- """
- if first_token == stop_token:
- return _rule_error(first_token, 'Stop token')
- if first_token.type == '{} block':
- prelude = []
- block = first_token
- else:
- prelude = [first_token]
- for token in tokens:
- if token == stop_token:
- return _rule_error(token, 'Stop token')
- if token.type == '{} block':
- block = token
- # TODO: handle special case for CSS variables (using "nested")
- # https://drafts.csswg.org/css-syntax-3/#consume-qualified-rule
- break
- prelude.append(token)
- else:
- return _rule_error(prelude[-1], 'EOF')
- return QualifiedRule(
- first_token.source_line, first_token.source_column, prelude, block.content)
|