Import python-cssselect2_0.2.1.orig.tar.gz
[dgit import orig python-cssselect2_0.2.1.orig.tar.gz]
This commit is contained in:
commit
4e8927610a
|
@ -0,0 +1,10 @@
|
|||
[run]
|
||||
branch = True
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
raise NotImplementedError
|
||||
omit =
|
||||
.*
|
|
@ -0,0 +1,10 @@
|
|||
*.pyc
|
||||
*.egg-info
|
||||
/.cache
|
||||
/.coverage
|
||||
/.eggs
|
||||
/build
|
||||
/dist
|
||||
/env
|
||||
/htmlcov
|
||||
/profile
|
|
@ -0,0 +1,30 @@
|
|||
language: python
|
||||
sudo: false
|
||||
|
||||
git:
|
||||
submodules: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
python: 2.7
|
||||
- os: linux
|
||||
python: 3.3
|
||||
- os: linux
|
||||
python: 3.4
|
||||
- os: linux
|
||||
python: 3.5
|
||||
- os: linux
|
||||
python: 3.6
|
||||
- os: osx
|
||||
language: generic
|
||||
env: PYTHON_VERSION=3
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install python3; fi
|
||||
|
||||
install:
|
||||
- pip$PYTHON_VERSION install --upgrade -e.[test]
|
||||
|
||||
script:
|
||||
- python$PYTHON_VERSION setup.py test
|
|
@ -0,0 +1,29 @@
|
|||
cssselect2 changelog
|
||||
====================
|
||||
|
||||
|
||||
Version 0.2.1
|
||||
-------------
|
||||
|
||||
Released on 2017-10-02.
|
||||
|
||||
* Fix documentation.
|
||||
|
||||
|
||||
Version 0.2.0
|
||||
-------------
|
||||
|
||||
Released on 2017-08-16.
|
||||
|
||||
* Fix some selectors for HTML documents with no namespace.
|
||||
* Don't crash when the attribute comparator is unknown.
|
||||
* Don't crash when there are empty attribute classes.
|
||||
* Follow semantic versioning.
|
||||
|
||||
|
||||
Version 0.1
|
||||
-----------
|
||||
|
||||
Released on 2017-07-07.
|
||||
|
||||
* Initial release.
|
|
@ -0,0 +1,31 @@
|
|||
Copyright (c) 2012 - 2013 by Simon Sapin, 2017 by Guillaume Ayoub.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,3 @@
|
|||
include README.rst CHANGES LICENSE tox.ini .coveragerc
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
|
@ -0,0 +1,39 @@
|
|||
Metadata-Version: 1.0
|
||||
Name: cssselect2
|
||||
Version: 0.2.1
|
||||
Summary: CSS selectors for Python ElementTree
|
||||
Home-page: http://packages.python.org/cssselect2/
|
||||
Author: Simon Sapin
|
||||
Author-email: simon.sapin@exyr.org
|
||||
License: BSD
|
||||
Description-Content-Type: UNKNOWN
|
||||
Description: cssselect2: CSS selectors for Python ElementTree
|
||||
################################################
|
||||
|
||||
cssselect2 is a straightforward implementation of `CSS3 Selectors`_ for markup
|
||||
documents (HTML, XML, etc.) that can be read by `ElementTree`_-like parsers
|
||||
(including cElementTree, lxml_, html5lib_, etc.)
|
||||
|
||||
Unlike cssselect_, it does not translate selectors to XPath_ and therefore does
|
||||
not have all the correctness corner cases that are hard or impossible to fix in
|
||||
cssselect.
|
||||
|
||||
.. _ElementTree: http://docs.python.org/3/library/xml.etree.elementtree.html
|
||||
.. _CSS3 Selectors: http://www.w3.org/TR/2011/REC-css3-selectors-20110929/
|
||||
.. _lxml: http://lxml.de/
|
||||
.. _html5lib: https://github.com/html5lib/html5lib-python
|
||||
.. _cssselect: http://packages.python.org/cssselect/
|
||||
.. _XPath: http://www.w3.org/TR/xpath/
|
||||
|
||||
|
||||
Quick facts:
|
||||
|
||||
* Free software: BSD licensed
|
||||
* Compatible with Python 2.7+ and 3.3+
|
||||
* Latest documentation: http://cssselect2.readthedocs.io/
|
||||
* Source, issues and pull requests `on Github
|
||||
<https://github.com/Kozea/cssselect2/>`_
|
||||
* Releases `on PyPI <http://pypi.python.org/pypi/cssselect2>`_
|
||||
* Install with ``pip install cssselect2``
|
||||
|
||||
Platform: UNKNOWN
|
|
@ -0,0 +1,28 @@
|
|||
cssselect2: CSS selectors for Python ElementTree
|
||||
################################################
|
||||
|
||||
cssselect2 is a straightforward implementation of `CSS3 Selectors`_ for markup
|
||||
documents (HTML, XML, etc.) that can be read by `ElementTree`_-like parsers
|
||||
(including cElementTree, lxml_, html5lib_, etc.)
|
||||
|
||||
Unlike cssselect_, it does not translate selectors to XPath_ and therefore does
|
||||
not have all the correctness corner cases that are hard or impossible to fix in
|
||||
cssselect.
|
||||
|
||||
.. _ElementTree: http://docs.python.org/3/library/xml.etree.elementtree.html
|
||||
.. _CSS3 Selectors: http://www.w3.org/TR/2011/REC-css3-selectors-20110929/
|
||||
.. _lxml: http://lxml.de/
|
||||
.. _html5lib: https://github.com/html5lib/html5lib-python
|
||||
.. _cssselect: http://packages.python.org/cssselect/
|
||||
.. _XPath: http://www.w3.org/TR/xpath/
|
||||
|
||||
|
||||
Quick facts:
|
||||
|
||||
* Free software: BSD licensed
|
||||
* Compatible with Python 2.7+ and 3.3+
|
||||
* Latest documentation: http://cssselect2.readthedocs.io/
|
||||
* Source, issues and pull requests `on Github
|
||||
<https://github.com/Kozea/cssselect2/>`_
|
||||
* Releases `on PyPI <http://pypi.python.org/pypi/cssselect2>`_
|
||||
* Install with ``pip install cssselect2``
|
|
@ -0,0 +1,39 @@
|
|||
Metadata-Version: 1.0
|
||||
Name: cssselect2
|
||||
Version: 0.2.1
|
||||
Summary: CSS selectors for Python ElementTree
|
||||
Home-page: http://packages.python.org/cssselect2/
|
||||
Author: Simon Sapin
|
||||
Author-email: simon.sapin@exyr.org
|
||||
License: BSD
|
||||
Description-Content-Type: UNKNOWN
|
||||
Description: cssselect2: CSS selectors for Python ElementTree
|
||||
################################################
|
||||
|
||||
cssselect2 is a straightforward implementation of `CSS3 Selectors`_ for markup
|
||||
documents (HTML, XML, etc.) that can be read by `ElementTree`_-like parsers
|
||||
(including cElementTree, lxml_, html5lib_, etc.)
|
||||
|
||||
Unlike cssselect_, it does not translate selectors to XPath_ and therefore does
|
||||
not have all the correctness corner cases that are hard or impossible to fix in
|
||||
cssselect.
|
||||
|
||||
.. _ElementTree: http://docs.python.org/3/library/xml.etree.elementtree.html
|
||||
.. _CSS3 Selectors: http://www.w3.org/TR/2011/REC-css3-selectors-20110929/
|
||||
.. _lxml: http://lxml.de/
|
||||
.. _html5lib: https://github.com/html5lib/html5lib-python
|
||||
.. _cssselect: http://packages.python.org/cssselect/
|
||||
.. _XPath: http://www.w3.org/TR/xpath/
|
||||
|
||||
|
||||
Quick facts:
|
||||
|
||||
* Free software: BSD licensed
|
||||
* Compatible with Python 2.7+ and 3.3+
|
||||
* Latest documentation: http://cssselect2.readthedocs.io/
|
||||
* Source, issues and pull requests `on Github
|
||||
<https://github.com/Kozea/cssselect2/>`_
|
||||
* Releases `on PyPI <http://pypi.python.org/pypi/cssselect2>`_
|
||||
* Install with ``pip install cssselect2``
|
||||
|
||||
Platform: UNKNOWN
|
|
@ -0,0 +1,31 @@
|
|||
.coveragerc
|
||||
.gitignore
|
||||
.travis.yml
|
||||
CHANGES
|
||||
LICENSE
|
||||
MANIFEST.in
|
||||
README.rst
|
||||
example.py
|
||||
setup.cfg
|
||||
setup.py
|
||||
cssselect2/__init__.py
|
||||
cssselect2/_compat.py
|
||||
cssselect2/compiler.py
|
||||
cssselect2/parser.py
|
||||
cssselect2/tree.py
|
||||
cssselect2.egg-info/PKG-INFO
|
||||
cssselect2.egg-info/SOURCES.txt
|
||||
cssselect2.egg-info/dependency_links.txt
|
||||
cssselect2.egg-info/requires.txt
|
||||
cssselect2.egg-info/top_level.txt
|
||||
cssselect2/tests/LICENSE
|
||||
cssselect2/tests/__init__.py
|
||||
cssselect2/tests/content.xhtml
|
||||
cssselect2/tests/ids.html
|
||||
cssselect2/tests/invalid_selectors.json
|
||||
cssselect2/tests/make_valid_selectors_json.sh
|
||||
cssselect2/tests/shakespeare.html
|
||||
cssselect2/tests/test_cssselect2.py
|
||||
cssselect2/tests/valid_selectors.json
|
||||
docs/conf.py
|
||||
docs/index.rst
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
tinycss2
|
||||
|
||||
[test]
|
||||
pytest-runner
|
||||
pytest-cov
|
||||
pytest-flake8
|
||||
pytest-isort
|
|
@ -0,0 +1 @@
|
|||
cssselect2
|
|
@ -0,0 +1,114 @@
|
|||
# coding: utf8
|
||||
"""
|
||||
cssselect2
|
||||
----------
|
||||
|
||||
CSS selectors for ElementTree.
|
||||
|
||||
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import operator
|
||||
|
||||
from webencodings import ascii_lower
|
||||
|
||||
# Classes are imported here to expose them at the top level of the module
|
||||
from .compiler import compile_selector_list # noqa
|
||||
from .parser import SelectorError # noqa
|
||||
from .tree import ElementWrapper # noqa
|
||||
|
||||
|
||||
VERSION = '0.2.1'
|
||||
|
||||
|
||||
class Matcher(object):
|
||||
"""A CSS selectors storage that can match against HTML elements."""
|
||||
def __init__(self):
|
||||
self.id_selectors = {}
|
||||
self.class_selectors = {}
|
||||
self.lower_local_name_selectors = {}
|
||||
self.namespace_selectors = {}
|
||||
self.other_selectors = []
|
||||
self.order = 0
|
||||
|
||||
def add_selector(self, selector, payload):
|
||||
"""
|
||||
Add a selector and its payload to the matcher.
|
||||
|
||||
:param selector:
|
||||
A :class:`CompiledSelector` object.
|
||||
:param payload:
|
||||
Some data associated to the selector,
|
||||
such as :class:`declarations <~tinycss2.ast.Declaration>`
|
||||
parsed from the :attr:`~tinycss2.ast.QualifiedRule.content`
|
||||
of a style rule.
|
||||
It can be any Python object,
|
||||
and will be returned as-is by :meth:`match`.
|
||||
|
||||
"""
|
||||
self.order += 1
|
||||
|
||||
if selector.never_matches:
|
||||
return
|
||||
|
||||
entry = (
|
||||
selector.test, selector.specificity, self.order,
|
||||
selector.pseudo_element, payload)
|
||||
if selector.id is not None:
|
||||
self.id_selectors.setdefault(selector.id, []).append(entry)
|
||||
elif selector.class_name is not None:
|
||||
self.class_selectors.setdefault(selector.class_name, []) \
|
||||
.append(entry)
|
||||
elif selector.local_name is not None:
|
||||
self.lower_local_name_selectors.setdefault(
|
||||
selector.lower_local_name, []).append(entry)
|
||||
elif selector.namespace is not None:
|
||||
self.namespace_selectors.setdefault(selector.namespace, []) \
|
||||
.append(entry)
|
||||
else:
|
||||
self.other_selectors.append(entry)
|
||||
|
||||
def match(self, element):
|
||||
"""
|
||||
Match selectors against the given element.
|
||||
|
||||
:param element:
|
||||
An :class:`ElementWrapper`.
|
||||
:returns:
|
||||
A list of the :obj:`payload` objects associated
|
||||
to selectors that match element,
|
||||
in order of lowest to highest :attr:`~CompiledSelector.specificity`
|
||||
and in order of addition with :meth:`add_selector`
|
||||
among selectors of equal specificity.
|
||||
|
||||
"""
|
||||
relevant_selectors = []
|
||||
|
||||
if element.id is not None:
|
||||
relevant_selectors.append(self.id_selectors.get(element.id, []))
|
||||
|
||||
for class_name in element.classes:
|
||||
relevant_selectors.append(self.class_selectors.get(class_name, []))
|
||||
|
||||
relevant_selectors.append(
|
||||
self.lower_local_name_selectors.get(
|
||||
ascii_lower(element.local_name), []))
|
||||
relevant_selectors.append(
|
||||
self.namespace_selectors.get(element.namespace_url, []))
|
||||
relevant_selectors.append(self.other_selectors)
|
||||
|
||||
results = [
|
||||
(specificity, order, pseudo, payload)
|
||||
for selector_list in relevant_selectors
|
||||
for test, specificity, order, pseudo, payload in selector_list
|
||||
if test(element)
|
||||
]
|
||||
results.sort(key=SORT_KEY)
|
||||
return results
|
||||
|
||||
|
||||
SORT_KEY = operator.itemgetter(0, 1)
|
|
@ -0,0 +1,9 @@
|
|||
try:
|
||||
basestring = basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
try:
|
||||
from itertools import ifilter
|
||||
except ImportError:
|
||||
ifilter = filter
|
|
@ -0,0 +1,336 @@
|
|||
# coding: utf8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from tinycss2.nth import parse_nth
|
||||
from webencodings import ascii_lower
|
||||
|
||||
from . import parser
|
||||
from .parser import SelectorError
|
||||
|
||||
# http://dev.w3.org/csswg/selectors/#whitespace
|
||||
split_whitespace = re.compile('[^ \t\r\n\f]+').findall
|
||||
|
||||
|
||||
def compile_selector_list(input, namespaces=None):
|
||||
"""Compile a (comma-separated) list of selectors.
|
||||
|
||||
:param input:
|
||||
A :term:`tinycss2:string`,
|
||||
or an iterable of tinycss2 :term:`tinycss2:component values` such as
|
||||
the :attr:`~tinycss2.ast.QualifiedRule.predule` of a style rule.
|
||||
:param namespaces:
|
||||
A optional dictionary of all `namespace prefix declarations
|
||||
<http://www.w3.org/TR/selectors/#nsdecl>`_ in scope for this selector.
|
||||
Keys are namespace prefixes as strings, or ``None`` for the default
|
||||
namespace.
|
||||
Values are namespace URLs as strings.
|
||||
If omitted, assume that no prefix is declared.
|
||||
:returns:
|
||||
A list of opaque :class:`CompiledSelector` objects.
|
||||
|
||||
"""
|
||||
return [
|
||||
CompiledSelector(selector)
|
||||
for selector in parser.parse(input, namespaces)
|
||||
]
|
||||
|
||||
|
||||
class CompiledSelector(object):
|
||||
def __init__(self, parsed_selector):
|
||||
source = _compile_node(parsed_selector.parsed_tree)
|
||||
self.never_matches = source == '0'
|
||||
self.test = eval(
|
||||
'lambda el: ' + source,
|
||||
{'split_whitespace': split_whitespace, 'ascii_lower': ascii_lower},
|
||||
{},
|
||||
)
|
||||
self.specificity = parsed_selector.specificity
|
||||
self.pseudo_element = parsed_selector.pseudo_element
|
||||
self.id = None
|
||||
self.class_name = None
|
||||
self.local_name = None
|
||||
self.lower_local_name = None
|
||||
self.namespace = None
|
||||
|
||||
node = parsed_selector.parsed_tree
|
||||
if isinstance(node, parser.CombinedSelector):
|
||||
node = node.right
|
||||
for simple_selector in node.simple_selectors:
|
||||
if isinstance(simple_selector, parser.IDSelector):
|
||||
self.id = simple_selector.ident
|
||||
elif isinstance(simple_selector, parser.ClassSelector):
|
||||
self.class_name = simple_selector.class_name
|
||||
elif isinstance(simple_selector, parser.LocalNameSelector):
|
||||
self.local_name = simple_selector.local_name
|
||||
self.lower_local_name = simple_selector.lower_local_name
|
||||
elif isinstance(simple_selector, parser.NamespaceSelector):
|
||||
self.namespace = simple_selector.namespace
|
||||
|
||||
|
||||
def _compile_node(selector):
|
||||
"""Return a boolean expression, as a Python source string.
|
||||
|
||||
When evaluated in a context where the `el` variable is an
|
||||
:class:`~cssselect2.tree.Element` object,
|
||||
tells whether the element is a subject of `selector`.
|
||||
|
||||
"""
|
||||
# To avoid precedence-related bugs, any sub-expression that is passed
|
||||
# around must be "atomic": add parentheses when the top-level would be
|
||||
# an operator. Bare literals and function calls are fine.
|
||||
|
||||
# 1 and 0 are used for True and False to avoid global lookups.
|
||||
|
||||
if isinstance(selector, parser.CombinedSelector):
|
||||
left_inside = _compile_node(selector.left)
|
||||
if left_inside == '0':
|
||||
return '0' # 0 and x == 0
|
||||
elif left_inside == '1':
|
||||
# 1 and x == x, but the element matching 1 still needs to exist.
|
||||
if selector.combinator in (' ', '>'):
|
||||
left = 'el.parent is not None'
|
||||
elif selector.combinator in ('~', '+'):
|
||||
left = 'el.previous is not None'
|
||||
else:
|
||||
raise SelectorError('Unknown combinator', selector.combinator)
|
||||
# Rebind the `el` name inside a generator-expressions (in a new scope)
|
||||
# so that 'left_inside' applies to different elements.
|
||||
elif selector.combinator == ' ':
|
||||
left = 'any((%s) for el in el.iter_ancestors())' % left_inside
|
||||
elif selector.combinator == '>':
|
||||
left = ('next(el is not None and (%s) for el in [el.parent])'
|
||||
% left_inside)
|
||||
elif selector.combinator == '+':
|
||||
left = ('next(el is not None and (%s) for el in [el.previous])'
|
||||
% left_inside)
|
||||
elif selector.combinator == '~':
|
||||
left = ('any((%s) for el in el.iter_previous_siblings())'
|
||||
% left_inside)
|
||||
else:
|
||||
raise SelectorError('Unknown combinator', selector.combinator)
|
||||
|
||||
right = _compile_node(selector.right)
|
||||
if right == '0':
|
||||
return '0' # 0 and x == 0
|
||||
elif right == '1':
|
||||
return left # 1 and x == x
|
||||
else:
|
||||
# Evaluate combinators right to left:
|
||||
return '(%s) and (%s)' % (right, left)
|
||||
|
||||
elif isinstance(selector, parser.CompoundSelector):
|
||||
sub_expressions = [
|
||||
expr for expr in map(_compile_node, selector.simple_selectors)
|
||||
if expr != '1']
|
||||
if len(sub_expressions) == 1:
|
||||
test = sub_expressions[0]
|
||||
elif '0' in sub_expressions:
|
||||
test = '0'
|
||||
elif sub_expressions:
|
||||
test = ' and '.join('(%s)' % e for e in sub_expressions)
|
||||
else:
|
||||
test = '1' # all([]) == True
|
||||
|
||||
if isinstance(selector, parser.NegationSelector):
|
||||
if test == '0':
|
||||
return '1'
|
||||
elif test == '1':
|
||||
return '0'
|
||||
else:
|
||||
return 'not (%s)' % test
|
||||
else:
|
||||
return test
|
||||
|
||||
elif isinstance(selector, parser.LocalNameSelector):
|
||||
return ('el.local_name == (%r if el.in_html_document else %r)'
|
||||
% (selector.lower_local_name, selector.local_name))
|
||||
|
||||
elif isinstance(selector, parser.NamespaceSelector):
|
||||
return 'el.namespace_url == %r' % selector.namespace
|
||||
|
||||
elif isinstance(selector, parser.ClassSelector):
|
||||
return '%r in el.classes' % selector.class_name
|
||||
|
||||
elif isinstance(selector, parser.IDSelector):
|
||||
return 'el.id == %r' % selector.ident
|
||||
|
||||
elif isinstance(selector, parser.AttributeSelector):
|
||||
if selector.namespace is not None:
|
||||
if selector.namespace:
|
||||
key = '(%r if el.in_html_document else %r)' % (
|
||||
'{%s}%s' % (selector.namespace, selector.lower_name),
|
||||
'{%s}%s' % (selector.namespace, selector.name),
|
||||
)
|
||||
else:
|
||||
key = ('(%r if el.in_html_document else %r)'
|
||||
% (selector.lower_name, selector.name))
|
||||
value = selector.value
|
||||
if selector.operator is None:
|
||||
return 'el.etree_element.get(%s) is not None' % key
|
||||
elif selector.operator == '=':
|
||||
return 'el.etree_element.get(%s) == %r' % (key, value)
|
||||
elif selector.operator == '~=':
|
||||
if len(value.split()) != 1 or value.strip() != value:
|
||||
return '0'
|
||||
else:
|
||||
return (
|
||||
'%r in split_whitespace(el.etree_element.get(%s, ""))'
|
||||
% (value, key))
|
||||
elif selector.operator == '|=':
|
||||
return ('next(v == %r or (v is not None and v.startswith(%r))'
|
||||
' for v in [el.etree_element.get(%s)])'
|
||||
% (value, value + '-', key))
|
||||
elif selector.operator == '^=':
|
||||
if value:
|
||||
return 'el.etree_element.get(%s, "").startswith(%r)' % (
|
||||
key, value)
|
||||
else:
|
||||
return '0'
|
||||
elif selector.operator == '$=':
|
||||
if value:
|
||||
return 'el.etree_element.get(%s, "").endswith(%r)' % (
|
||||
key, value)
|
||||
else:
|
||||
return '0'
|
||||
elif selector.operator == '*=':
|
||||
if value:
|
||||
return '%r in el.etree_element.get(%s, "")' % (value, key)
|
||||
else:
|
||||
return '0'
|
||||
else:
|
||||
raise SelectorError(
|
||||
'Unknown attribute operator', selector.operator)
|
||||
else: # In any namespace
|
||||
raise NotImplementedError # TODO
|
||||
|
||||
elif isinstance(selector, parser.PseudoClassSelector):
|
||||
if selector.name == 'link':
|
||||
return ('%s and el.etree_element.get("href") is not None'
|
||||
% html_tag_eq('a', 'area', 'link'))
|
||||
elif selector.name == 'enabled':
|
||||
return (
|
||||
'(%s and el.etree_element.get("disabled") is None'
|
||||
' and not el.in_disabled_fieldset) or'
|
||||
'(%s and el.etree_element.get("disabled") is None) or '
|
||||
'(%s and el.etree_element.get("href") is not None)'
|
||||
% (
|
||||
html_tag_eq('button', 'input', 'select', 'textarea',
|
||||
'option'),
|
||||
html_tag_eq('optgroup', 'menuitem', 'fieldset'),
|
||||
html_tag_eq('a', 'area', 'link'),
|
||||
)
|
||||
)
|
||||
elif selector.name == 'disabled':
|
||||
return (
|
||||
'(%s and (el.etree_element.get("disabled") is not None'
|
||||
' or el.in_disabled_fieldset)) or'
|
||||
'(%s and el.etree_element.get("disabled") is not None)' % (
|
||||
html_tag_eq('button', 'input', 'select', 'textarea',
|
||||
'option'),
|
||||
html_tag_eq('optgroup', 'menuitem', 'fieldset'),
|
||||
)
|
||||
)
|
||||
elif selector.name == 'checked':
|
||||
return (
|
||||
'(%s and el.etree_element.get("checked") is not None and'
|
||||
' ascii_lower(el.etree_element.get("type", "")) '
|
||||
' in ("checkbox", "radio"))'
|
||||
'or (%s and el.etree_element.get("selected") is not None)'
|
||||
% (
|
||||
html_tag_eq('input', 'menuitem'),
|
||||
html_tag_eq('option'),
|
||||
)
|
||||
)
|
||||
elif selector.name in ('visited', 'hover', 'active', 'focus',
|
||||
'target'):
|
||||
# Not applicable in a static context: never match.
|
||||
return '0'
|
||||
elif selector.name == 'root':
|
||||
return 'el.parent is None'
|
||||
elif selector.name == 'first-child':
|
||||
return 'el.index == 0'
|
||||
elif selector.name == 'last-child':
|
||||
return 'el.index + 1 == len(el.etree_siblings)'
|
||||
elif selector.name == 'first-of-type':
|
||||
return ('all(s.tag != el.etree_element.tag'
|
||||
' for s in el.etree_siblings[:el.index])')
|
||||
elif selector.name == 'last-of-type':
|
||||
return ('all(s.tag != el.etree_element.tag'
|
||||
' for s in el.etree_siblings[el.index + 1:])')
|
||||
elif selector.name == 'only-child':
|
||||
return 'len(el.etree_siblings) == 1'
|
||||
elif selector.name == 'only-of-type':
|
||||
return ('all(s.tag != el.etree_element.tag or i == el.index'
|
||||
' for i, s in enumerate(el.etree_siblings))')
|
||||
elif selector.name == 'empty':
|
||||
return 'not (el.etree_children or el.etree_element.text)'
|
||||
else:
|
||||
raise SelectorError('Unknown pseudo-class', selector.name)
|
||||
|
||||
elif isinstance(selector, parser.FunctionalPseudoClassSelector):
|
||||
if selector.name == 'lang':
|
||||
tokens = [
|
||||
t for t in selector.arguments
|
||||
if t.type != 'whitespace'
|
||||
]
|
||||
if len(tokens) == 1 and tokens[0].type == 'ident':
|
||||
lang = tokens[0].lower_value
|
||||
else:
|
||||
raise SelectorError('Invalid arguments for :lang()')
|
||||
|
||||
return ('el.lang == %r or el.lang.startswith(%r)'
|
||||
% (lang, lang + '-'))
|
||||
else:
|
||||
if selector.name == 'nth-child':
|
||||
count = 'el.index'
|
||||
elif selector.name == 'nth-last-child':
|
||||
count = '(len(el.etree_siblings) - el.index - 1)'
|
||||
elif selector.name == 'nth-of-type':
|
||||
count = ('sum(1 for s in el.etree_siblings[:el.index]'
|
||||
' if s.tag == el.etree_element.tag)')
|
||||
elif selector.name == 'nth-last-of-type':
|
||||
count = ('sum(1 for s in el.etree_siblings[el.index + 1:]'
|
||||
' if s.tag == el.etree_element.tag)')
|
||||
else:
|
||||
raise SelectorError('Unknown pseudo-class', selector.name)
|
||||
|
||||
result = parse_nth(selector.arguments)
|
||||
if result is None:
|
||||
raise SelectorError(
|
||||
'Invalid arguments for :%s()' % selector.name)
|
||||
a, b = result
|
||||
# x is the number of siblings before/after the element
|
||||
# Matches if a positive or zero integer n exists so that:
|
||||
# x = a*n + b-1
|
||||
# x = a*n + B
|
||||
B = b - 1
|
||||
if a == 0:
|
||||
# x = B
|
||||
return '%s == %i' % (count, B)
|
||||
else:
|
||||
# n = (x - B) / a
|
||||
return ('next(r == 0 and n >= 0'
|
||||
' for n, r in [divmod(%s - %i, %i)])'
|
||||
% (count, B, a))
|
||||
|
||||
else:
|
||||
raise TypeError(type(selector), selector)
|
||||
|
||||
|
||||
def html_tag_eq(*local_names):
|
||||
if len(local_names) == 1:
|
||||
return (
|
||||
'((el.local_name == %r) if el.in_html_document else '
|
||||
'(el.etree_element.tag == %r))' % (
|
||||
local_names[0],
|
||||
'{http://www.w3.org/1999/xhtml}' + local_names[0]))
|
||||
else:
|
||||
return (
|
||||
'((el.local_name in (%s)) if el.in_html_document else '
|
||||
'(el.etree_element.tag in (%s)))' % (
|
||||
', '.join(repr(n) for n in local_names),
|
||||
', '.join(repr('{http://www.w3.org/1999/xhtml}' + n)
|
||||
for n in local_names)))
|
|
@ -0,0 +1,427 @@
|
|||
# coding: utf8
|
||||
"""
|
||||
cssselect2.parser
|
||||
-----------------
|
||||
|
||||
A parser for CSS selectors, based on the tinycss tokenizer.
|
||||
|
||||
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from tinycss2 import parse_component_value_list
|
||||
|
||||
from ._compat import basestring
|
||||
|
||||
__all__ = ['parse']
|
||||
|
||||
|
||||
def parse(input, namespaces=None):
|
||||
"""
|
||||
:param input:
|
||||
A :term:`string`, or an iterable of :term:`component values`.
|
||||
"""
|
||||
if isinstance(input, basestring):
|
||||
input = parse_component_value_list(input)
|
||||
tokens = TokenStream(input)
|
||||
namespaces = namespaces or {}
|
||||
yield parse_selector(tokens, namespaces)
|
||||
tokens.skip_whitespace_and_comment()
|
||||
while 1:
|
||||
next = tokens.next()
|
||||
if next is None:
|
||||
return
|
||||
elif next == ',':
|
||||
yield parse_selector(tokens, namespaces)
|
||||
else:
|
||||
raise SelectorError(next, 'unpexpected %s token.' % next.type)
|
||||
|
||||
|
||||
def parse_selector(tokens, namespaces):
|
||||
result, pseudo_element = parse_compound_selector(tokens, namespaces)
|
||||
while 1:
|
||||
has_whitespace = tokens.skip_whitespace()
|
||||
while tokens.skip_comment():
|
||||
has_whitespace = tokens.skip_whitespace() or has_whitespace
|
||||
if pseudo_element is not None:
|
||||
return Selector(result, pseudo_element)
|
||||
peek = tokens.peek()
|
||||
if peek is None or peek == ',':
|
||||
return Selector(result, pseudo_element)
|
||||
elif peek in ('>', '+', '~'):
|
||||
combinator = peek.value
|
||||
tokens.next()
|
||||
elif has_whitespace:
|
||||
combinator = ' '
|
||||
else:
|
||||
return Selector(result, pseudo_element)
|
||||
compound, pseudo_element = parse_compound_selector(tokens, namespaces)
|
||||
result = CombinedSelector(result, combinator, compound)
|
||||
|
||||
|
||||
def parse_compound_selector(tokens, namespaces):
|
||||
type_selectors = parse_type_selector(tokens, namespaces)
|
||||
simple_selectors = type_selectors if type_selectors is not None else []
|
||||
while 1:
|
||||
simple_selector, pseudo_element = parse_simple_selector(
|
||||
tokens, namespaces)
|
||||
if pseudo_element is not None or simple_selector is None:
|
||||
break
|
||||
simple_selectors.append(simple_selector)
|
||||
|
||||
if (simple_selectors or type_selectors is not None or
|
||||
pseudo_element is not None):
|
||||
return CompoundSelector(simple_selectors), pseudo_element
|
||||
else:
|
||||
peek = tokens.peek()
|
||||
raise SelectorError(peek, 'expected a compound selector, got %s'
|
||||
% (peek.type if peek else 'EOF'))
|
||||
|
||||
|
||||
def parse_type_selector(tokens, namespaces):
|
||||
tokens.skip_whitespace()
|
||||
qualified_name = parse_qualified_name(tokens, namespaces)
|
||||
if qualified_name is None:
|
||||
return None
|
||||
|
||||
simple_selectors = []
|
||||
namespace, local_name = qualified_name
|
||||
if local_name is not None:
|
||||
simple_selectors.append(LocalNameSelector(local_name))
|
||||
if namespace is not None:
|
||||
simple_selectors.append(NamespaceSelector(namespace))
|
||||
return simple_selectors
|
||||
|
||||
|
||||
def parse_simple_selector(tokens, namespaces, in_negation=False):
|
||||
peek = tokens.peek()
|
||||
if peek is None:
|
||||
return None, None
|
||||
if peek.type == 'hash' and peek.is_identifier:
|
||||
tokens.next()
|
||||
return IDSelector(peek.value), None
|
||||
elif peek == '.':
|
||||
tokens.next()
|
||||
next = tokens.next()
|
||||
if next is None or next.type != 'ident':
|
||||
raise SelectorError(
|
||||
next, 'Expected a class name, got %s' % next)
|
||||
return ClassSelector(next.value), None
|
||||
elif peek.type == '[] block':
|
||||
tokens.next()
|
||||
attr = parse_attribute_selector(TokenStream(peek.content), namespaces)
|
||||
return attr, None
|
||||
elif peek == ':':
|
||||
tokens.next()
|
||||
next = tokens.next()
|
||||
if next == ':':
|
||||
next = tokens.next()
|
||||
if next is None or next.type != 'ident':
|
||||
raise SelectorError(
|
||||
next, 'Expected a pseudo-element name, got %s' % next)
|
||||
return None, next.lower_value
|
||||
elif next is not None and next.type == 'ident':
|
||||
name = next.lower_value
|
||||
if name in ('before', 'after', 'first-line', 'first-letter'):
|
||||
return None, name
|
||||
else:
|
||||
return PseudoClassSelector(name), None
|
||||
elif next is not None and next.type == 'function':
|
||||
name = next.lower_name
|
||||
if name == 'not':
|
||||
if in_negation:
|
||||
raise SelectorError(next, 'nested :not()')
|
||||
return parse_negation(next, namespaces), None
|
||||
else:
|
||||
return (
|
||||
FunctionalPseudoClassSelector(name, next.arguments), None)
|
||||
else:
|
||||
raise SelectorError(next, 'unexpected %s token.' % next)
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_negation(negation_token, namespaces):
|
||||
tokens = TokenStream(negation_token.arguments)
|
||||
type_selectors = parse_type_selector(tokens, namespaces)
|
||||
if type_selectors is not None:
|
||||
return NegationSelector(type_selectors)
|
||||
|
||||
simple_selector, pseudo_element = parse_simple_selector(
|
||||
tokens, namespaces, in_negation=True)
|
||||
tokens.skip_whitespace()
|
||||
if pseudo_element is None and tokens.next() is None:
|
||||
return NegationSelector([simple_selector])
|
||||
else:
|
||||
raise SelectorError(
|
||||
negation_token, ':not() only accepts a simple selector')
|
||||
|
||||
|
||||
def parse_attribute_selector(tokens, namespaces):
|
||||
tokens.skip_whitespace()
|
||||
qualified_name = parse_qualified_name(
|
||||
tokens, namespaces, is_attribute=True)
|
||||
if qualified_name is None:
|
||||
next = tokens.next()
|
||||
raise SelectorError(
|
||||
next, 'expected attribute name, got %s' % next)
|
||||
namespace, local_name = qualified_name
|
||||
|
||||
tokens.skip_whitespace()
|
||||
peek = tokens.peek()
|
||||
if peek is None:
|
||||
operator = None
|
||||
value = None
|
||||
elif peek in ('=', '~=', '|=', '^=', '$=', '*='):
|
||||
operator = peek.value
|
||||
tokens.next()
|
||||
tokens.skip_whitespace()
|
||||
next = tokens.next()
|
||||
if next is None or next.type not in ('ident', 'string'):
|
||||
next_type = 'None' if next is None else next.type
|
||||
raise SelectorError(
|
||||
next, 'expected attribute value, got %s' % next_type)
|
||||
value = next.value
|
||||
else:
|
||||
raise SelectorError(
|
||||
peek, 'expected attribute selector operator, got %s' % peek)
|
||||
|
||||
tokens.skip_whitespace()
|
||||
next = tokens.next()
|
||||
if next is not None:
|
||||
raise SelectorError(next, 'expected ], got %s' % next.type)
|
||||
return AttributeSelector(namespace, local_name, operator, value)
|
||||
|
||||
|
||||
def parse_qualified_name(tokens, namespaces, is_attribute=False):
|
||||
"""Returns None (not a qualified name) or (ns, local),
|
||||
in which None is a wildcard. The empty string for ns is "no namespace".
|
||||
|
||||
"""
|
||||
peek = tokens.peek()
|
||||
if peek is None:
|
||||
return None
|
||||
if peek.type == 'ident':
|
||||
first_ident = tokens.next()
|
||||
peek = tokens.peek()
|
||||
if peek != '|':
|
||||
namespace = '' if is_attribute else namespaces.get(None, None)
|
||||
return namespace, (first_ident.value, first_ident.lower_value)
|
||||
tokens.next()
|
||||
namespace = namespaces.get(first_ident.value)
|
||||
if namespace is None:
|
||||
raise SelectorError(
|
||||
first_ident,
|
||||
'undefined namespace prefix: ' + first_ident.value)
|
||||
elif peek == '*':
|
||||
next = tokens.next()
|
||||
peek = tokens.peek()
|
||||
if peek != '|':
|
||||
if is_attribute:
|
||||
raise SelectorError(
|
||||
next, 'Expected local name, got %s' % next.type)
|
||||
return namespaces.get(None, None), None
|
||||
tokens.next()
|
||||
namespace = None
|
||||
elif peek == '|':
|
||||
tokens.next()
|
||||
namespace = ''
|
||||
else:
|
||||
return None
|
||||
|
||||
# If we get here, we just consumed '|' and set ``namespace``
|
||||
next = tokens.next()
|
||||
if next.type == 'ident':
|
||||
return namespace, (next.value, next.lower_value)
|
||||
elif next == '*' and not is_attribute:
|
||||
return namespace, None
|
||||
else:
|
||||
raise SelectorError(next, 'Expected local name, got %s' % next.type)
|
||||
|
||||
|
||||
class SelectorError(ValueError):
|
||||
"""A specialized ``ValueError`` for invalid selectors."""
|
||||
|
||||
|
||||
class TokenStream(object):
|
||||
def __init__(self, tokens):
|
||||
self.tokens = iter(tokens)
|
||||
self.peeked = [] # In reversed order
|
||||
|
||||
def next(self):
|
||||
if self.peeked:
|
||||
return self.peeked.pop()
|
||||
else:
|
||||
return next(self.tokens, None)
|
||||
|
||||
def peek(self):
|
||||
if not self.peeked:
|
||||
self.peeked.append(next(self.tokens, None))
|
||||
return self.peeked[-1]
|
||||
|
||||
def skip(self, skip_types):
|
||||
found = False
|
||||
while 1:
|
||||
peek = self.peek()
|
||||
if peek is None or peek.type not in skip_types:
|
||||
break
|
||||
self.next()
|
||||
found = True
|
||||
return found
|
||||
|
||||
def skip_whitespace(self):
|
||||
return self.skip(['whitespace'])
|
||||
|
||||
def skip_comment(self):
|
||||
return self.skip(['comment'])
|
||||
|
||||
def skip_whitespace_and_comment(self):
|
||||
return self.skip(['comment', 'whitespace'])
|
||||
|
||||
|
||||
class Selector(object):
|
||||
def __init__(self, tree, pseudo_element=None):
|
||||
self.parsed_tree = tree
|
||||
if pseudo_element is None:
|
||||
self.pseudo_element = pseudo_element
|
||||
#: Tuple of 3 integers: http://www.w3.org/TR/selectors/#specificity
|
||||
self.specificity = tree.specificity
|
||||
else:
|
||||
self.pseudo_element = pseudo_element
|
||||
a, b, c = tree.specificity
|
||||
self.specificity = a, b, c + 1
|
||||
|
||||
def __repr__(self):
|
||||
if self.pseudo_element is None:
|
||||
return repr(self.parsed_tree)
|
||||
else:
|
||||
return '%r::%s' % (self.parsed_tree, self.pseudo_element)
|
||||
|
||||
|
||||
class CombinedSelector(object):
|
||||
def __init__(self, left, combinator, right):
|
||||
#: Combined or compound selector
|
||||
self.left = left
|
||||
# One of `` `` (a single space), ``>``, ``+`` or ``~``.
|
||||
self.combinator = combinator
|
||||
#: compound selector
|
||||
self.right = right
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
a1, b1, c1 = self.left.specificity
|
||||
a2, b2, c2 = self.right.specificity
|
||||
return a1 + a2, b1 + b2, c1 + c2
|
||||
|
||||
def __repr__(self):
|
||||
return '%r%s%r' % (self.left, self.combinator, self.right)
|
||||
|
||||
|
||||
class CompoundSelector(object):
|
||||
"""Aka. sequence of simple selectors, in Level 3."""
|
||||
def __init__(self, simple_selectors):
|
||||
self.simple_selectors = simple_selectors
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
if self.simple_selectors:
|
||||
# zip(*foo) turns [(a1, b1, c1), (a2, b2, c2), ...]
|
||||
# into [(a1, a2, ...), (b1, b2, ...), (c1, c2, ...)]
|
||||
return tuple(map(sum, zip(
|
||||
*(sel.specificity for sel in self.simple_selectors))))
|
||||
else:
|
||||
return 0, 0, 0
|
||||
|
||||
def __repr__(self):
|
||||
return ''.join(map(repr, self.simple_selectors))
|
||||
|
||||
|
||||
class LocalNameSelector(object):
|
||||
specificity = 0, 0, 1
|
||||
|
||||
def __init__(self, local_name):
|
||||
self.local_name, self.lower_local_name = local_name
|
||||
|
||||
def __repr__(self):
|
||||
return self.local_name
|
||||
|
||||
|
||||
class NamespaceSelector(object):
|
||||
specificity = 0, 0, 0
|
||||
|
||||
def __init__(self, namespace):
|
||||
#: The namespace URL as a string,
|
||||
#: or the empty string for elements not in any namespace.
|
||||
self.namespace = namespace
|
||||
|
||||
def __repr__(self):
|
||||
if self.namespace == '':
|
||||
return '|'
|
||||
else:
|
||||
return '{%s}|' % self.namespace
|
||||
|
||||
|
||||
class IDSelector(object):
|
||||
specificity = 1, 0, 0
|
||||
|
||||
def __init__(self, ident):
|
||||
self.ident = ident
|
||||
|
||||
def __repr__(self):
|
||||
return '#' + self.ident
|
||||
|
||||
|
||||
class ClassSelector(object):
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, class_name):
|
||||
self.class_name = class_name
|
||||
|
||||
def __repr__(self):
|
||||
return '.' + self.class_name
|
||||
|
||||
|
||||
class AttributeSelector(object):
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, namespace, name, operator, value):
|
||||
self.namespace = namespace
|
||||
self.name, self.lower_name = name
|
||||
#: A string like ``=`` or ``~=``, or None for ``[attr]`` selectors
|
||||
self.operator = operator
|
||||
#: A string, or None for ``[attr]`` selectors
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
namespace = ('*|' if self.namespace is None
|
||||
else '{%s}' % self.namespace)
|
||||
return '[%s%s%s%r]' % (namespace, self.name, self.operator, self.value)
|
||||
|
||||
|
||||
class PseudoClassSelector(object):
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return ':' + self.name
|
||||
|
||||
|
||||
class FunctionalPseudoClassSelector(object):
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, name, arguments):
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
|
||||
def __repr__(self):
|
||||
return ':%s%r' % (self.name, tuple(self.arguments))
|
||||
|
||||
|
||||
class NegationSelector(CompoundSelector):
|
||||
def __repr__(self):
|
||||
return ':not(%r)' % CompoundSelector.__repr__(self)
|
|
@ -0,0 +1,4 @@
|
|||
These files are taken form the web-platform-test repository
|
||||
and used under a 3-clause BSD License.
|
||||
|
||||
https://github.com/w3c/web-platform-tests/tree/master/selectors-api
|
|
@ -0,0 +1,11 @@
|
|||
# coding: utf8
|
||||
"""
|
||||
cssselect2.tests
|
||||
----------------
|
||||
|
||||
Test suite for cssselect2.
|
||||
|
||||
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
|
||||
"""
|
|
@ -0,0 +1,372 @@
|
|||
<!DOCTYPE html>
|
||||
<html id="html" lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head id="head">
|
||||
<title id="title">Selectors-API Test Suite: HTML with Selectors Level 2 using TestHarness: Test Document</title>
|
||||
|
||||
<!-- Links for :link and :visited pseudo-class test -->
|
||||
<link id="pseudo-link-link1" href=""/>
|
||||
<link id="pseudo-link-link2" href="http://example.org/"/>
|
||||
<link id="pseudo-link-link3"/>
|
||||
</head>
|
||||
<body id="body">
|
||||
<div id="root">
|
||||
<div id="target"></div>
|
||||
|
||||
<div id="universal">
|
||||
<p id="universal-p1">Universal selector tests inside element with <code id="universal-code1">id="universal"</code>.</p>
|
||||
<hr id="universal-hr1"/>
|
||||
<pre id="universal-pre1">Some preformatted text with some <span id="universal-span1">embedded code</span></pre>
|
||||
<p id="universal-p2">This is a normal link: <a id="universal-a1" href="http://www.w3.org/">W3C</a></p>
|
||||
<address id="universal-address1">Some more nested elements <code id="universal-code2"><a href="#" id="universal-a2">code hyperlink</a></code></address>
|
||||
</div>
|
||||
|
||||
<div id="attr-presence">
|
||||
<div class="attr-presence-div1" id="attr-presence-div1" align="center"></div>
|
||||
<div class="attr-presence-div2" id="attr-presence-div2" align=""></div>
|
||||
<div class="attr-presence-div3" id="attr-presence-div3" valign="center"></div>
|
||||
<div class="attr-presence-div4" id="attr-presence-div4" alignv="center"></div>
|
||||
<p id="attr-presence-p1"><a id="attr-presence-a1" tItLe=""></a><span id="attr-presence-span1" TITLE="attr-presence-span1"></span></p>
|
||||
<pre id="attr-presence-pre1" data-attr-presence="pre1"></pre>
|
||||
<blockquote id="attr-presence-blockquote1" data-attr-presence="blockquote1"></blockquote>
|
||||
<ul id="attr-presence-ul1" data-中文=""></ul>
|
||||
|
||||
<select id="attr-presence-select1">
|
||||
<option id="attr-presence-select1-option1">A</option>
|
||||
<option id="attr-presence-select1-option2">B</option>
|
||||
<option id="attr-presence-select1-option3">C</option>
|
||||
<option id="attr-presence-select1-option4">D</option>
|
||||
</select>
|
||||
<select id="attr-presence-select2">
|
||||
<option id="attr-presence-select2-option1">A</option>
|
||||
<option id="attr-presence-select2-option2">B</option>
|
||||
<option id="attr-presence-select2-option3">C</option>
|
||||
<option id="attr-presence-select2-option4" selected="selected">D</option>
|
||||
</select>
|
||||
<select id="attr-presence-select3" multiple="multiple">
|
||||
<option id="attr-presence-select3-option1">A</option>
|
||||
<option id="attr-presence-select3-option2" selected="">B</option>
|
||||
<option id="attr-presence-select3-option3" selected="selected">C</option>
|
||||
<option id="attr-presence-select3-option4">D</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="attr-value">
|
||||
<div id="attr-value-div1" align="center"></div>
|
||||
<div id="attr-value-div2" align=""></div>
|
||||
<div id="attr-value-div3" data-attr-value="é"></div>
|
||||
<div id="attr-value-div4" data-attr-value_foo="é"></div>
|
||||
|
||||
<form id="attr-value-form1">
|
||||
<input id="attr-value-input1" type="text"/>
|
||||
<input id="attr-value-input2" type="password"/>
|
||||
<input id="attr-value-input3" type="hidden"/>
|
||||
<input id="attr-value-input4" type="radio"/>
|
||||
<input id="attr-value-input5" type="checkbox"/>
|
||||
<input id="attr-value-input6" type="radio"/>
|
||||
<input id="attr-value-input7" type="text"/>
|
||||
<input id="attr-value-input8" type="hidden"/>
|
||||
<input id="attr-value-input9" type="radio"/>
|
||||
</form>
|
||||
|
||||
<div id="attr-value-div5" data-attr-value="中文"></div>
|
||||
</div>
|
||||
|
||||
<div id="attr-whitespace">
|
||||
<div id="attr-whitespace-div1" class="foo div1 bar"></div>
|
||||
<div id="attr-whitespace-div2" class=""></div>
|
||||
<div id="attr-whitespace-div3" class="foo div3 bar"></div>
|
||||
|
||||
<div id="attr-whitespace-div4" data-attr-whitespace="foo é bar"></div>
|
||||
<div id="attr-whitespace-div5" data-attr-whitespace_foo="é foo"></div>
|
||||
|
||||
<a id="attr-whitespace-a1" rel="next bookmark"></a>
|
||||
<a id="attr-whitespace-a2" rel="tag nofollow"></a>
|
||||
<a id="attr-whitespace-a3" rel="tag bookmark"></a>
|
||||
<a id="attr-whitespace-a4" rel="book mark"></a> <!-- Intentional space in "book mark" -->
|
||||
<a id="attr-whitespace-a5" rel="nofollow"></a>
|
||||
<a id="attr-whitespace-a6" rev="bookmark nofollow"></a>
|
||||
<a id="attr-whitespace-a7" rel="prev next tag alternate nofollow author help icon noreferrer prefetch search stylesheet tag"></a>
|
||||
|
||||
<p id="attr-whitespace-p1" title="Chinese 中文 characters"></p>
|
||||
</div>
|
||||
|
||||
<div id="attr-hyphen">
|
||||
<div id="attr-hyphen-div1"></div>
|
||||
<div id="attr-hyphen-div2" lang="fr"></div>
|
||||
<div id="attr-hyphen-div3" lang="en-AU"></div>
|
||||
<div id="attr-hyphen-div4" lang="es"></div>
|
||||
</div>
|
||||
|
||||
<div id="attr-begins">
|
||||
<a id="attr-begins-a1" href="http://www.example.org"></a>
|
||||
<a id="attr-begins-a2" href="http://example.org/"></a>
|
||||
<a id="attr-begins-a3" href="http://www.example.com/"></a>
|
||||
|
||||
<div id="attr-begins-div1" lang="fr"></div>
|
||||
<div id="attr-begins-div2" lang="en-AU"></div>
|
||||
<div id="attr-begins-div3" lang="es"></div>
|
||||
<div id="attr-begins-div4" lang="en-US"></div>
|
||||
<div id="attr-begins-div5" lang="en"></div>
|
||||
|
||||
<p id="attr-begins-p1" class=" apple"></p> <!-- Intentional space in class value " apple". -->
|
||||
</div>
|
||||
|
||||
<div id="attr-ends">
|
||||
<a id="attr-ends-a1" href="http://www.example.org"></a>
|
||||
<a id="attr-ends-a2" href="http://example.org/"></a>
|
||||
<a id="attr-ends-a3" href="http://www.example.org"></a>
|
||||
|
||||
<div id="attr-ends-div1" lang="fr"></div>
|
||||
<div id="attr-ends-div2" lang="de-CH"></div>
|
||||
<div id="attr-ends-div3" lang="es"></div>
|
||||
<div id="attr-ends-div4" lang="fr-CH"></div>
|
||||
|
||||
<p id="attr-ends-p1" class="apple "></p> <!-- Intentional space in class value "apple ". -->
|
||||
</div>
|
||||
|
||||
<div id="attr-contains">
|
||||
<a id="attr-contains-a1" href="http://www.example.org"></a>
|
||||
<a id="attr-contains-a2" href="http://example.org/"></a>
|
||||
<a id="attr-contains-a3" href="http://www.example.com/"></a>
|
||||
|
||||
<div id="attr-contains-div1" lang="fr"></div>
|
||||
<div id="attr-contains-div2" lang="en-AU"></div>
|
||||
<div id="attr-contains-div3" lang="de-CH"></div>
|
||||
<div id="attr-contains-div4" lang="es"></div>
|
||||
<div id="attr-contains-div5" lang="fr-CH"></div>
|
||||
<div id="attr-contains-div6" lang="en-US"></div>
|
||||
|
||||
<p id="attr-contains-p1" class=" apple banana orange "></p>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-nth">
|
||||
<table id="pseudo-nth-table1">
|
||||
<tr id="pseudo-nth-tr1"><td id="pseudo-nth-td1"></td><td id="pseudo-nth-td2"></td><td id="pseudo-nth-td3"></td><td id="pseudo-nth-td4"></td><td id="pseudo-nth--td5"></td><td id="pseudo-nth-td6"></td></tr>
|
||||
<tr id="pseudo-nth-tr2"><td id="pseudo-nth-td7"></td><td id="pseudo-nth-td8"></td><td id="pseudo-nth-td9"></td><td id="pseudo-nth-td10"></td><td id="pseudo-nth-td11"></td><td id="pseudo-nth-td12"></td></tr>
|
||||
<tr id="pseudo-nth-tr3"><td id="pseudo-nth-td13"></td><td id="pseudo-nth-td14"></td><td id="pseudo-nth-td15"></td><td id="pseudo-nth-td16"></td><td id="pseudo-nth-td17"></td><td id="pseudo-nth-td18"></td></tr>
|
||||
</table>
|
||||
|
||||
<ol id="pseudo-nth-ol1">
|
||||
<li id="pseudo-nth-li1"></li>
|
||||
<li id="pseudo-nth-li2"></li>
|
||||
<li id="pseudo-nth-li3"></li>
|
||||
<li id="pseudo-nth-li4"></li>
|
||||
<li id="pseudo-nth-li5"></li>
|
||||
<li id="pseudo-nth-li6"></li>
|
||||
<li id="pseudo-nth-li7"></li>
|
||||
<li id="pseudo-nth-li8"></li>
|
||||
<li id="pseudo-nth-li9"></li>
|
||||
<li id="pseudo-nth-li10"></li>
|
||||
<li id="pseudo-nth-li11"></li>
|
||||
<li id="pseudo-nth-li12"></li>
|
||||
</ol>
|
||||
|
||||
<p id="pseudo-nth-p1">
|
||||
<span id="pseudo-nth-span1">span1</span>
|
||||
<em id="pseudo-nth-em1">em1</em>
|
||||
<!-- comment node-->
|
||||
<em id="pseudo-nth-em2">em2</em>
|
||||
<span id="pseudo-nth-span2">span2</span>
|
||||
<strong id="pseudo-nth-strong1">strong1</strong>
|
||||
<em id="pseudo-nth-em3">em3</em>
|
||||
<span id="pseudo-nth-span3">span3</span>
|
||||
<span id="pseudo-nth-span4">span4</span>
|
||||
<strong id="pseudo-nth-strong2">strong2</strong>
|
||||
<em id="pseudo-nth-em4">em4</em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-first-child">
|
||||
<div id="pseudo-first-child-div1"></div>
|
||||
<div id="pseudo-first-child-div2"></div>
|
||||
<div id="pseudo-first-child-div3"></div>
|
||||
|
||||
<p id="pseudo-first-child-p1"><span id="pseudo-first-child-span1"></span><span id="pseudo-first-child-span2"></span></p>
|
||||
<p id="pseudo-first-child-p2"><span id="pseudo-first-child-span3"></span><span id="pseudo-first-child-span4"></span></p>
|
||||
<p id="pseudo-first-child-p3"><span id="pseudo-first-child-span5"></span><span id="pseudo-first-child-span6"></span></p>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-last-child">
|
||||
<p id="pseudo-last-child-p1"><span id="pseudo-last-child-span1"></span><span id="pseudo-last-child-span2"></span></p>
|
||||
<p id="pseudo-last-child-p2"><span id="pseudo-last-child-span3"></span><span id="pseudo-last-child-span4"></span></p>
|
||||
<p id="pseudo-last-child-p3"><span id="pseudo-last-child-span5"></span><span id="pseudo-last-child-span6"></span></p>
|
||||
|
||||
<div id="pseudo-last-child-div1"></div>
|
||||
<div id="pseudo-last-child-div2"></div>
|
||||
<div id="pseudo-last-child-div3"></div>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-only">
|
||||
<p id="pseudo-only-p1">
|
||||
<span id="pseudo-only-span1"></span>
|
||||
</p>
|
||||
<p id="pseudo-only-p2">
|
||||
<span id="pseudo-only-span2"></span>
|
||||
<span id="pseudo-only-span3"></span>
|
||||
</p>
|
||||
<p id="pseudo-only-p3">
|
||||
<span id="pseudo-only-span4"></span>
|
||||
<em id="pseudo-only-em1"></em>
|
||||
<span id="pseudo-only-span5"></span>
|
||||
</p>
|
||||
</div>>
|
||||
|
||||
<div id="pseudo-empty">
|
||||
<p id="pseudo-empty-p1"></p>
|
||||
<p id="pseudo-empty-p2"><!-- comment node --></p>
|
||||
<p id="pseudo-empty-p3"> </p>
|
||||
<p id="pseudo-empty-p4">Text node</p>
|
||||
<p id="pseudo-empty-p5"><span id="pseudo-empty-span1"></span></p>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-link">
|
||||
<a id="pseudo-link-a1" href="">with href</a>
|
||||
<a id="pseudo-link-a2" href="http://example.org/">with href</a>
|
||||
<a id="pseudo-link-a3">without href</a>
|
||||
<map name="pseudo-link-map1" id="pseudo-link-map1">
|
||||
<area id="pseudo-link-area1" href=""/>
|
||||
<area id="pseudo-link-area2"/>
|
||||
</map>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-lang">
|
||||
<div id="pseudo-lang-div1"></div>
|
||||
<div id="pseudo-lang-div2" lang="fr"></div>
|
||||
<div id="pseudo-lang-div3" lang="en-AU"></div>
|
||||
<div id="pseudo-lang-div4" lang="es"></div>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-ui">
|
||||
<input id="pseudo-ui-input1" type="text"/>
|
||||
<input id="pseudo-ui-input2" type="password"/>
|
||||
<input id="pseudo-ui-input3" type="radio"/>
|
||||
<input id="pseudo-ui-input4" type="radio" checked="checked"/>
|
||||
<input id="pseudo-ui-input5" type="checkbox"/>
|
||||
<input id="pseudo-ui-input6" type="checkbox" checked="checked"/>
|
||||
<input id="pseudo-ui-input7" type="submit"/>
|
||||
<input id="pseudo-ui-input8" type="button"/>
|
||||
<input id="pseudo-ui-input9" type="hidden"/>
|
||||
<textarea id="pseudo-ui-textarea1"></textarea>
|
||||
<button id="pseudo-ui-button1">Enabled</button>
|
||||
|
||||
<input id="pseudo-ui-input10" disabled="disabled" type="text"/>
|
||||
<input id="pseudo-ui-input11" disabled="disabled" type="password"/>
|
||||
<input id="pseudo-ui-input12" disabled="disabled" type="radio"/>
|
||||
<input id="pseudo-ui-input13" disabled="disabled" type="radio" checked="checked"/>
|
||||
<input id="pseudo-ui-input14" disabled="disabled" type="checkbox"/>
|
||||
<input id="pseudo-ui-input15" disabled="disabled" type="checkbox" checked="checked"/>
|
||||
<input id="pseudo-ui-input16" disabled="disabled" type="submit"/>
|
||||
<input id="pseudo-ui-input17" disabled="disabled" type="button"/>
|
||||
<input id="pseudo-ui-input18" disabled="disabled" type="hidden"/>
|
||||
<textarea id="pseudo-ui-textarea2" disabled="disabled"></textarea>
|
||||
<button id="pseudo-ui-button2" disabled="disabled">Disabled</button>
|
||||
</div>
|
||||
|
||||
<div id="not">
|
||||
<div id="not-div1"></div>
|
||||
<div id="not-div2"></div>
|
||||
<div id="not-div3"></div>
|
||||
|
||||
<p id="not-p1"><span id="not-span1"></span><em id="not-em1"></em></p>
|
||||
<p id="not-p2"><span id="not-span2"></span><em id="not-em2"></em></p>
|
||||
<p id="not-p3"><span id="not-span3"></span><em id="not-em3"></em></p>
|
||||
</div>
|
||||
|
||||
<div id="pseudo-element">All pseudo-element tests</div>
|
||||
|
||||
<div id="class">
|
||||
<p id="class-p1" class="foo class-p bar"></p>
|
||||
<p id="class-p2" class="class-p foo bar"></p>
|
||||
<p id="class-p3" class="foo bar class-p"></p>
|
||||
|
||||
<!-- All permutations of the classes should match -->
|
||||
<div id="class-div1" class="apple orange banana"></div>
|
||||
<div id="class-div2" class="apple banana orange"></div>
|
||||
<p id="class-p4" class="orange apple banana"></p>
|
||||
<div id="class-div3" class="orange banana apple"></div>
|
||||
<p id="class-p6" class="banana apple orange"></p>
|
||||
<div id="class-div4" class="banana orange apple"></div>
|
||||
<div id="class-div5" class="apple orange"></div>
|
||||
<div id="class-div6" class="apple banana"></div>
|
||||
<div id="class-div7" class="orange banana"></div>
|
||||
|
||||
<span id="class-span1" class="台北Táiběi 台北"></span>
|
||||
<span id="class-span2" class="台北"></span>
|
||||
|
||||
<span id="class-span3" class="foo:bar"></span>
|
||||
<span id="class-span4" class="test.foo[5]bar"></span>
|
||||
</div>
|
||||
|
||||
<div id="id">
|
||||
<div id="id-div1"></div>
|
||||
<div id="id-div2"></div>
|
||||
|
||||
<ul id="id-ul1">
|
||||
<li id="id-li-duplicate"></li>
|
||||
<li id="id-li-duplicate"></li>
|
||||
<li id="id-li-duplicate"></li>
|
||||
<li id="id-li-duplicate"></li>
|
||||
</ul>
|
||||
|
||||
<span id="台北Táiběi"></span>
|
||||
<span id="台北"></span>
|
||||
|
||||
<span id="#foo:bar"></span>
|
||||
<span id="test.foo[5]bar"></span>
|
||||
</div>
|
||||
|
||||
<div id="descendant">
|
||||
<div id="descendant-div1" class="descendant-div1">
|
||||
<div id="descendant-div2" class="descendant-div2">
|
||||
<div id="descendant-div3" class="descendant-div3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="descendant-div4" class="descendant-div4"></div>
|
||||
</div>
|
||||
|
||||
<div id="child">
|
||||
<div id="child-div1" class="child-div1">
|
||||
<div id="child-div2" class="child-div2">
|
||||
<div id="child-div3" class="child-div3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="child-div4" class="child-div4"></div>
|
||||
</div>
|
||||
|
||||
<div id="adjacent">
|
||||
<div id="adjacent-div1" class="adjacent-div1"></div>
|
||||
<div id="adjacent-div2" class="adjacent-div2">
|
||||
<div id="adjacent-div3" class="adjacent-div3"></div>
|
||||
</div>
|
||||
<div id="adjacent-div4" class="adjacent-div4">
|
||||
<p id="adjacent-p1" class="adjacent-p1"></p>
|
||||
<div id="adjacent-div5" class="adjacent-div5"></div>
|
||||
</div>
|
||||
<div id="adjacent-div6" class="adjacent-div6"></div>
|
||||
<p id="adjacent-p2" class="adjacent-p2"></p>
|
||||
<p id="adjacent-p3" class="adjacent-p3"></p>
|
||||
</div>
|
||||
|
||||
<div id="sibling">
|
||||
<div id="sibling-div1" class="sibling-div"></div>
|
||||
<div id="sibling-div2" class="sibling-div">
|
||||
<div id="sibling-div3" class="sibling-div"></div>
|
||||
</div>
|
||||
<div id="sibling-div4" class="sibling-div">
|
||||
<p id="sibling-p1" class="sibling-p"></p>
|
||||
<div id="sibling-div5" class="sibling-div"></div>
|
||||
</div>
|
||||
<div id="sibling-div6" class="sibling-div"></div>
|
||||
<p id="sibling-p2" class="sibling-p"></p>
|
||||
<p id="sibling-p3" class="sibling-p"></p>
|
||||
</div>
|
||||
|
||||
<div id="group">
|
||||
<em id="group-em1"></em>
|
||||
<strong id="group-strong1"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,48 @@
|
|||
<html id="html" xmlns="http://www.w3.org/1999/xhtml"><head>
|
||||
<link id="link-href" href="foo" />
|
||||
<link id="link-nohref" />
|
||||
</head><body>
|
||||
<div id="outer-div">
|
||||
<a id="name-anchor" name="foo"></a>
|
||||
<a id="tag-anchor" rel="tag" href="http://localhost/foo">link</a>
|
||||
<a id="nofollow-anchor" rel="nofollow" href="https://example.org">
|
||||
link</a>
|
||||
<ol id="first-ol" class="a b c">
|
||||
<li id="first-li">content</li>
|
||||
<li id="second-li" lang="En-us">
|
||||
<div id="li-div">
|
||||
</div>
|
||||
</li>
|
||||
<li id="third-li" class="ab c"></li>
|
||||
<li id="fourth-li" class="ab
|
||||
c"></li>
|
||||
<li id="fifth-li"></li>
|
||||
<li id="sixth-li"></li>
|
||||
<li id="seventh-li"> </li>
|
||||
</ol>
|
||||
<p id="paragraph">
|
||||
<b id="p-b">hi</b> <em id="p-em">there</em>
|
||||
<b id="p-b2">guy</b>
|
||||
<input type="checkbox" id="checkbox-unchecked" />
|
||||
<input type="checkbox" id="checkbox-disabled" disabled="" />
|
||||
<input type="text" id="text-checked" checked="checked" />
|
||||
<input type="hidden" id="input-hidden" />
|
||||
<input type="hidden" id="input-hidden-disabled" disabled="disabled" />
|
||||
<input type="checkbox" id="checkbox-checked" checked="checked" />
|
||||
<input type="checkbox" id="checkbox-disabled-checked"
|
||||
disabled="disabled" checked="checked" />
|
||||
<fieldset id="fieldset" disabled="disabled">
|
||||
<input type="checkbox" id="checkbox-fieldset-disabled" />
|
||||
<input type="hidden" id="hidden-fieldset-disabled" />
|
||||
</fieldset>
|
||||
</p>
|
||||
<ol id="second-ol">
|
||||
</ol>
|
||||
<map name="dummymap">
|
||||
<area shape="circle" coords="200,250,25" href="foo.html" id="area-href" />
|
||||
<area shape="default" id="area-nohref" />
|
||||
</map>
|
||||
</div>
|
||||
<div id="foobar-div" foobar="ab bc
|
||||
cde"><span id="foobar-span"></span></div>
|
||||
</body></html>
|
|
@ -0,0 +1,36 @@
|
|||
[
|
||||
{"name": "Empty String", "selector": ""},
|
||||
{"name": "Invalid character", "selector": "["},
|
||||
{"name": "Invalid character", "selector": "]"},
|
||||
{"name": "Invalid character", "selector": "("},
|
||||
{"name": "Invalid character", "selector": ")"},
|
||||
{"name": "Invalid character", "selector": "{"},
|
||||
{"name": "Invalid character", "selector": "}"},
|
||||
{"name": "Invalid character", "selector": "<"},
|
||||
{"name": "Invalid character", "selector": ">"},
|
||||
{"name": "Invalid character", "selector": ":"},
|
||||
{"name": "Invalid character", "selector": "::"},
|
||||
{"name": "Invalid ID", "selector": "#"},
|
||||
{"name": "Invalid group of selectors", "selector": "div,"},
|
||||
{"name": "Invalid class", "selector": "."},
|
||||
{"name": "Invalid class", "selector": ".5cm"},
|
||||
{"name": "Invalid class", "selector": "..test"},
|
||||
{"name": "Invalid class", "selector": ".foo..quux"},
|
||||
{"name": "Invalid class", "selector": ".bar."},
|
||||
{"name": "Invalid combinator", "selector": "div & address, p"},
|
||||
{"name": "Invalid combinator", "selector": "div >> address, p"},
|
||||
{"name": "Invalid combinator", "selector": "div ++ address, p"},
|
||||
{"name": "Invalid combinator", "selector": "div ~~ address, p"},
|
||||
{"name": "Invalid [att=value] selector", "selector": "[*=test]"},
|
||||
{"name": "Invalid [att=value] selector", "selector": "[*|*=test]"},
|
||||
{"name": "Invalid [att=value] selector", "selector": "[class= space unquoted ]"},
|
||||
{"name": "Unknown pseudo-class", "selector": "div:example"},
|
||||
{"name": "Unknown pseudo-class", "selector": ":example"},
|
||||
{"name": "Unknown pseudo-element", "selector": "div::example", "xfail": true},
|
||||
{"name": "Unknown pseudo-element", "selector": "::example", "xfail": true},
|
||||
{"name": "Invalid pseudo-element", "selector": ":::before"},
|
||||
{"name": "Undeclared namespace", "selector": "ns|div"},
|
||||
{"name": "Undeclared namespace", "selector": ":not(ns|div)"},
|
||||
{"name": "Invalid namespace", "selector": "^|div"},
|
||||
{"name": "Invalid namespace", "selector": "$|div"}
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
WEB_PLATFORM_TESTS="$1"
|
||||
|
||||
if [ -f "$WEB_PLATFORM_TESTS/selectors-api/selectors.js" ]
|
||||
then
|
||||
(
|
||||
cat "$WEB_PLATFORM_TESTS/selectors-api/selectors.js"
|
||||
echo "validSelectors.map(function(selector) {"
|
||||
echo " delete selector.testType;"
|
||||
echo "});"
|
||||
echo "console.log(JSON.stringify(validSelectors, null, ' '))"
|
||||
) | node
|
||||
else
|
||||
echo "Usage: $0 path/to/web-plateform-test"
|
||||
exit;
|
||||
fi
|
|
@ -0,0 +1,307 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" debug="true">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test">
|
||||
<div class="dialog">
|
||||
<h2>As You Like It</h2>
|
||||
<div id="playwright">
|
||||
by William Shakespeare
|
||||
</div>
|
||||
<div class="dialog scene thirdClass" id="scene1">
|
||||
<h3>ACT I, SCENE III. A room in the palace.</h3>
|
||||
<div class="dialog">
|
||||
<div class="direction">Enter CELIA and ROSALIND</div>
|
||||
</div>
|
||||
<div id="speech1" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.1">Why, cousin! why, Rosalind! Cupid have mercy! not a word?</div>
|
||||
</div>
|
||||
<div id="speech2" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.2">Not one to throw at a dog.</div>
|
||||
</div>
|
||||
<div id="speech3" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.3">No, thy words are too precious to be cast away upon</div>
|
||||
<div id="scene1.3.4">curs; throw some of them at me; come, lame me with reasons.</div>
|
||||
</div>
|
||||
<div id="speech4" class="character">ROSALIND</div>
|
||||
<div id="speech5" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.8">But is all this for your father?</div>
|
||||
</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.5">Then there were two cousins laid up; when the one</div>
|
||||
<div id="scene1.3.6">should be lamed with reasons and the other mad</div>
|
||||
<div id="scene1.3.7">without any.</div>
|
||||
</div>
|
||||
<div id="speech6" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.9">No, some of it is for my child's father. O, how</div>
|
||||
<div id="scene1.3.10">full of briers is this working-day world!</div>
|
||||
</div>
|
||||
<div id="speech7" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.11">They are but burs, cousin, thrown upon thee in</div>
|
||||
<div id="scene1.3.12">holiday foolery: if we walk not in the trodden</div>
|
||||
<div id="scene1.3.13">paths our very petticoats will catch them.</div>
|
||||
</div>
|
||||
<div id="speech8" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.14">I could shake them off my coat: these burs are in my heart.</div>
|
||||
</div>
|
||||
<div id="speech9" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.15">Hem them away.</div>
|
||||
</div>
|
||||
<div id="speech10" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.16">I would try, if I could cry 'hem' and have him.</div>
|
||||
</div>
|
||||
<div id="speech11" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.17">Come, come, wrestle with thy affections.</div>
|
||||
</div>
|
||||
<div id="speech12" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.18">O, they take the part of a better wrestler than myself!</div>
|
||||
</div>
|
||||
<div id="speech13" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.19">O, a good wish upon you! you will try in time, in</div>
|
||||
<div id="scene1.3.20">despite of a fall. But, turning these jests out of</div>
|
||||
<div id="scene1.3.21">service, let us talk in good earnest: is it</div>
|
||||
<div id="scene1.3.22">possible, on such a sudden, you should fall into so</div>
|
||||
<div id="scene1.3.23">strong a liking with old Sir Rowland's youngest son?</div>
|
||||
</div>
|
||||
<div id="speech14" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.24">The duke my father loved his father dearly.</div>
|
||||
</div>
|
||||
<div id="speech15" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.25">Doth it therefore ensue that you should love his son</div>
|
||||
<div id="scene1.3.26">dearly? By this kind of chase, I should hate him,</div>
|
||||
<div id="scene1.3.27">for my father hated his father dearly; yet I hate</div>
|
||||
<div id="scene1.3.28">not Orlando.</div>
|
||||
</div>
|
||||
<div id="speech16" class="character">ROSALIND</div>
|
||||
<div title="wtf" class="dialog">
|
||||
<div id="scene1.3.29">No, faith, hate him not, for my sake.</div>
|
||||
</div>
|
||||
<div id="speech17" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.30">Why should I not? doth he not deserve well?</div>
|
||||
</div>
|
||||
<div id="speech18" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.31">Let me love him for that, and do you love him</div>
|
||||
<div id="scene1.3.32">because I do. Look, here comes the duke.</div>
|
||||
</div>
|
||||
<div id="speech19" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.33">With his eyes full of anger.</div>
|
||||
<div class="direction">Enter DUKE FREDERICK, with Lords</div>
|
||||
</div>
|
||||
<div id="speech20" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.34">Mistress, dispatch you with your safest haste</div>
|
||||
<div id="scene1.3.35">And get you from our court.</div>
|
||||
</div>
|
||||
<div id="speech21" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.36">Me, uncle?</div>
|
||||
</div>
|
||||
<div id="speech22" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.37">You, cousin</div>
|
||||
<div id="scene1.3.38">Within these ten days if that thou be'st found</div>
|
||||
<div id="scene1.3.39">So near our public court as twenty miles,</div>
|
||||
<div id="scene1.3.40">Thou diest for it.</div>
|
||||
</div>
|
||||
<div id="speech23" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.41"> I do beseech your grace,</div>
|
||||
<div id="scene1.3.42">Let me the knowledge of my fault bear with me:</div>
|
||||
<div id="scene1.3.43">If with myself I hold intelligence</div>
|
||||
<div id="scene1.3.44">Or have acquaintance with mine own desires,</div>
|
||||
<div id="scene1.3.45">If that I do not dream or be not frantic,--</div>
|
||||
<div id="scene1.3.46">As I do trust I am not--then, dear uncle,</div>
|
||||
<div id="scene1.3.47">Never so much as in a thought unborn</div>
|
||||
<div id="scene1.3.48">Did I offend your highness.</div>
|
||||
</div>
|
||||
<div id="speech24" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.49">Thus do all traitors:</div>
|
||||
<div id="scene1.3.50">If their purgation did consist in words,</div>
|
||||
<div id="scene1.3.51">They are as innocent as grace itself:</div>
|
||||
<div id="scene1.3.52">Let it suffice thee that I trust thee not.</div>
|
||||
</div>
|
||||
<div id="speech25" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.53">Yet your mistrust cannot make me a traitor:</div>
|
||||
<div id="scene1.3.54">Tell me whereon the likelihood depends.</div>
|
||||
</div>
|
||||
<div id="speech26" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.55">Thou art thy father's daughter; there's enough.</div>
|
||||
</div>
|
||||
<div id="speech27" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.56">So was I when your highness took his dukedom;</div>
|
||||
<div id="scene1.3.57">So was I when your highness banish'd him:</div>
|
||||
<div id="scene1.3.58">Treason is not inherited, my lord;</div>
|
||||
<div id="scene1.3.59">Or, if we did derive it from our friends,</div>
|
||||
<div id="scene1.3.60">What's that to me? my father was no traitor:</div>
|
||||
<div id="scene1.3.61">Then, good my liege, mistake me not so much</div>
|
||||
<div id="scene1.3.62">To think my poverty is treacherous.</div>
|
||||
</div>
|
||||
<div id="speech28" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.63">Dear sovereign, hear me speak.</div>
|
||||
</div>
|
||||
<div id="speech29" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.64">Ay, Celia; we stay'd her for your sake,</div>
|
||||
<div id="scene1.3.65">Else had she with her father ranged along.</div>
|
||||
</div>
|
||||
<div id="speech30" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.66">I did not then entreat to have her stay;</div>
|
||||
<div id="scene1.3.67">It was your pleasure and your own remorse:</div>
|
||||
<div id="scene1.3.68">I was too young that time to value her;</div>
|
||||
<div id="scene1.3.69">But now I know her: if she be a traitor,</div>
|
||||
<div id="scene1.3.70">Why so am I; we still have slept together,</div>
|
||||
<div id="scene1.3.71">Rose at an instant, learn'd, play'd, eat together,</div>
|
||||
<div id="scene1.3.72">And wheresoever we went, like Juno's swans,</div>
|
||||
<div id="scene1.3.73">Still we went coupled and inseparable.</div>
|
||||
</div>
|
||||
<div id="speech31" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.74">She is too subtle for thee; and her smoothness,</div>
|
||||
<div id="scene1.3.75">Her very silence and her patience</div>
|
||||
<div id="scene1.3.76">Speak to the people, and they pity her.</div>
|
||||
<div id="scene1.3.77">Thou art a fool: she robs thee of thy name;</div>
|
||||
<div id="scene1.3.78">And thou wilt show more bright and seem more virtuous</div>
|
||||
<div id="scene1.3.79">When she is gone. Then open not thy lips:</div>
|
||||
<div id="scene1.3.80">Firm and irrevocable is my doom</div>
|
||||
<div id="scene1.3.81">Which I have pass'd upon her; she is banish'd.</div>
|
||||
</div>
|
||||
<div id="speech32" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.82">Pronounce that sentence then on me, my liege:</div>
|
||||
<div id="scene1.3.83">I cannot live out of her company.</div>
|
||||
</div>
|
||||
<div id="speech33" class="character">DUKE FREDERICK</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.84">You are a fool. You, niece, provide yourself:</div>
|
||||
<div id="scene1.3.85">If you outstay the time, upon mine honour,</div>
|
||||
<div id="scene1.3.86">And in the greatness of my word, you die.</div>
|
||||
<div class="direction">Exeunt DUKE FREDERICK and Lords</div>
|
||||
</div>
|
||||
<div id="speech34" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.87">O my poor Rosalind, whither wilt thou go?</div>
|
||||
<div id="scene1.3.88">Wilt thou change fathers? I will give thee mine.</div>
|
||||
<div id="scene1.3.89">I charge thee, be not thou more grieved than I am.</div>
|
||||
</div>
|
||||
<div id="speech35" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.90">I have more cause.</div>
|
||||
</div>
|
||||
<div id="speech36" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.91"> Thou hast not, cousin;</div>
|
||||
<div id="scene1.3.92">Prithee be cheerful: know'st thou not, the duke</div>
|
||||
<div id="scene1.3.93">Hath banish'd me, his daughter?</div>
|
||||
</div>
|
||||
<div id="speech37" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.94">That he hath not.</div>
|
||||
</div>
|
||||
<div id="speech38" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.95">No, hath not? Rosalind lacks then the love</div>
|
||||
<div id="scene1.3.96">Which teacheth thee that thou and I am one:</div>
|
||||
<div id="scene1.3.97">Shall we be sunder'd? shall we part, sweet girl?</div>
|
||||
<div id="scene1.3.98">No: let my father seek another heir.</div>
|
||||
<div id="scene1.3.99">Therefore devise with me how we may fly,</div>
|
||||
<div id="scene1.3.100">Whither to go and what to bear with us;</div>
|
||||
<div id="scene1.3.101">And do not seek to take your change upon you,</div>
|
||||
<div id="scene1.3.102">To bear your griefs yourself and leave me out;</div>
|
||||
<div id="scene1.3.103">For, by this heaven, now at our sorrows pale,</div>
|
||||
<div id="scene1.3.104">Say what thou canst, I'll go along with thee.</div>
|
||||
</div>
|
||||
<div id="speech39" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.105">Why, whither shall we go?</div>
|
||||
</div>
|
||||
<div id="speech40" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.106">To seek my uncle in the forest of Arden.</div>
|
||||
</div>
|
||||
<div id="speech41" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.107">Alas, what danger will it be to us,</div>
|
||||
<div id="scene1.3.108">Maids as we are, to travel forth so far!</div>
|
||||
<div id="scene1.3.109">Beauty provoketh thieves sooner than gold.</div>
|
||||
</div>
|
||||
<div id="speech42" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.110">I'll put myself in poor and mean attire</div>
|
||||
<div id="scene1.3.111">And with a kind of umber smirch my face;</div>
|
||||
<div id="scene1.3.112">The like do you: so shall we pass along</div>
|
||||
<div id="scene1.3.113">And never stir assailants.</div>
|
||||
</div>
|
||||
<div id="speech43" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.114">Were it not better,</div>
|
||||
<div id="scene1.3.115">Because that I am more than common tall,</div>
|
||||
<div id="scene1.3.116">That I did suit me all points like a man?</div>
|
||||
<div id="scene1.3.117">A gallant curtle-axe upon my thigh,</div>
|
||||
<div id="scene1.3.118">A boar-spear in my hand; and--in my heart</div>
|
||||
<div id="scene1.3.119">Lie there what hidden woman's fear there will--</div>
|
||||
<div id="scene1.3.120">We'll have a swashing and a martial outside,</div>
|
||||
<div id="scene1.3.121">As many other mannish cowards have</div>
|
||||
<div id="scene1.3.122">That do outface it with their semblances.</div>
|
||||
</div>
|
||||
<div id="speech44" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.123">What shall I call thee when thou art a man?</div>
|
||||
</div>
|
||||
<div id="speech45" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.124">I'll have no worse a name than Jove's own page;</div>
|
||||
<div id="scene1.3.125">And therefore look you call me Ganymede.</div>
|
||||
<div id="scene1.3.126">But what will you be call'd?</div>
|
||||
</div>
|
||||
<div id="speech46" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.127">Something that hath a reference to my state</div>
|
||||
<div id="scene1.3.128">No longer Celia, but Aliena.</div>
|
||||
</div>
|
||||
<div id="speech47" class="character">ROSALIND</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.129">But, cousin, what if we assay'd to steal</div>
|
||||
<div id="scene1.3.130">The clownish fool out of your father's court?</div>
|
||||
<div id="scene1.3.131">Would he not be a comfort to our travel?</div>
|
||||
</div>
|
||||
<div id="speech48" class="character">CELIA</div>
|
||||
<div class="dialog">
|
||||
<div id="scene1.3.132">He'll go along o'er the wide world with me;</div>
|
||||
<div id="scene1.3.133">Leave me alone to woo him. Let's away,</div>
|
||||
<div id="scene1.3.134">And get our jewels and our wealth together,</div>
|
||||
<div id="scene1.3.135">Devise the fittest time and safest way</div>
|
||||
<div id="scene1.3.136">To hide us from pursuit that will be made</div>
|
||||
<div id="scene1.3.137">After my flight. Now go we in content</div>
|
||||
<div id="scene1.3.138">To liberty and not to banishment.</div>
|
||||
<div class="direction">Exeunt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,317 @@
|
|||
# coding: utf8
|
||||
"""
|
||||
cssselect2.tests
|
||||
----------------
|
||||
|
||||
Test suite for cssselect2.
|
||||
|
||||
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
import pytest
|
||||
|
||||
from cssselect2 import ElementWrapper, SelectorError, compile_selector_list
|
||||
|
||||
|
||||
def resource(filename):
|
||||
return os.path.join(os.path.dirname(__file__), filename)
|
||||
|
||||
|
||||
def load_json(filename):
|
||||
return json.load(open(resource(filename)))
|
||||
|
||||
|
||||
def get_test_document():
|
||||
document = etree.parse(resource('content.xhtml'))
|
||||
parent = next(e for e in document.getiterator() if e.get('id') == 'root')
|
||||
|
||||
# Setup namespace tests
|
||||
for id in ('any-namespace', 'no-namespace'):
|
||||
div = etree.SubElement(parent, '{http://www.w3.org/1999/xhtml}div')
|
||||
div.set('id', id)
|
||||
etree.SubElement(div, '{http://www.w3.org/1999/xhtml}div') \
|
||||
.set('id', id + '-div1')
|
||||
etree.SubElement(div, '{http://www.w3.org/1999/xhtml}div') \
|
||||
.set('id', id + '-div2')
|
||||
etree.SubElement(div, 'div').set('id', id + '-div3')
|
||||
etree.SubElement(div, '{http://www.example.org/ns}div') \
|
||||
.set('id', id + '-div4')
|
||||
|
||||
return document
|
||||
|
||||
|
||||
TEST_DOCUMENT = get_test_document()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test', load_json('invalid_selectors.json'))
|
||||
def test_invalid_selectors(test):
|
||||
if test.get('xfail'):
|
||||
pytest.xfail()
|
||||
try:
|
||||
compile_selector_list(test['selector'])
|
||||
except SelectorError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Should be invalid: %(selector)r %(name)s' % test)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test', load_json('valid_selectors.json'))
|
||||
def test_valid_selectors(test):
|
||||
if test.get('xfail'):
|
||||
pytest.xfail()
|
||||
exclude = test.get('exclude', ())
|
||||
if 'document' in exclude or 'xhtml' in exclude:
|
||||
return
|
||||
root = ElementWrapper.from_xml_root(TEST_DOCUMENT)
|
||||
result = [e.id for e in root.query_all(test['selector'])]
|
||||
if result != test['expect']:
|
||||
print(test['selector'])
|
||||
print(result)
|
||||
print('!=')
|
||||
print(test['expect'])
|
||||
raise AssertionError(test['name'])
|
||||
|
||||
|
||||
def test_lang():
|
||||
doc = etree.fromstring('''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"></html>
|
||||
''')
|
||||
assert not ElementWrapper.from_xml_root(doc).matches(':lang(fr)')
|
||||
|
||||
doc = etree.fromstring('''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<meta http-equiv="Content-Language" content=" fr \t"/>
|
||||
</html>
|
||||
''')
|
||||
root = ElementWrapper.from_xml_root(doc, content_language='en')
|
||||
assert root.matches(':lang(fr)')
|
||||
|
||||
doc = etree.fromstring('''
|
||||
<html>
|
||||
<meta http-equiv="Content-Language" content=" fr \t"/>
|
||||
</html>
|
||||
''')
|
||||
root = ElementWrapper.from_xml_root(doc, content_language='en')
|
||||
assert root.matches(':lang(en)')
|
||||
|
||||
doc = etree.fromstring('<html></html>')
|
||||
root = ElementWrapper.from_xml_root(doc, content_language='en')
|
||||
assert root.matches(':lang(en)')
|
||||
|
||||
root = ElementWrapper.from_xml_root(doc, content_language='en, es')
|
||||
assert not root.matches(':lang(en)')
|
||||
|
||||
root = ElementWrapper.from_xml_root(doc)
|
||||
assert not root.matches(':lang(en)')
|
||||
|
||||
doc = etree.fromstring('<html lang="eN"></html>')
|
||||
root = ElementWrapper.from_html_root(doc)
|
||||
assert root.matches(':lang(en)')
|
||||
|
||||
doc = etree.fromstring('<html lang="eN"></html>')
|
||||
root = ElementWrapper.from_xml_root(doc)
|
||||
assert not root.matches(':lang(en)')
|
||||
|
||||
|
||||
def test_select():
|
||||
root = etree.fromstring(HTML_IDS)
|
||||
|
||||
def select_ids(selector, html_only):
|
||||
xml_ids = [element.etree_element.get('id', 'nil') for element in
|
||||
ElementWrapper.from_xml_root(root).query_all(selector)]
|
||||
html_ids = [element.etree_element.get('id', 'nil') for element in
|
||||
ElementWrapper.from_html_root(root).query_all(selector)]
|
||||
if html_only:
|
||||
assert xml_ids == []
|
||||
else:
|
||||
assert xml_ids == html_ids
|
||||
return html_ids
|
||||
|
||||
def pcss(main, *selectors, **kwargs):
|
||||
html_only = kwargs.pop('html_only', False)
|
||||
result = select_ids(main, html_only)
|
||||
for selector in selectors:
|
||||
assert select_ids(selector, html_only) == result
|
||||
return result
|
||||
|
||||
all_ids = pcss('*')
|
||||
assert all_ids[:6] == [
|
||||
'html', 'nil', 'link-href', 'link-nohref', 'nil', 'outer-div']
|
||||
assert all_ids[-1:] == ['foobar-span']
|
||||
assert pcss('div') == ['outer-div', 'li-div', 'foobar-div']
|
||||
assert pcss('DIV', html_only=True) == [
|
||||
'outer-div', 'li-div', 'foobar-div'] # case-insensitive in HTML
|
||||
assert pcss('div div') == ['li-div']
|
||||
assert pcss('div, div div') == ['outer-div', 'li-div', 'foobar-div']
|
||||
assert pcss('div , div div') == ['outer-div', 'li-div', 'foobar-div']
|
||||
assert pcss('a[name]') == ['name-anchor']
|
||||
assert pcss('a[NAme]', html_only=True) == [
|
||||
'name-anchor'] # case-insensitive in HTML:
|
||||
assert pcss('a[rel]') == ['tag-anchor', 'nofollow-anchor']
|
||||
assert pcss('a[rel="tag"]') == ['tag-anchor']
|
||||
assert pcss('a[href*="localhost"]') == ['tag-anchor']
|
||||
assert pcss('a[href*=""]') == []
|
||||
assert pcss('a[href^="http"]') == ['tag-anchor', 'nofollow-anchor']
|
||||
assert pcss('a[href^="http:"]') == ['tag-anchor']
|
||||
assert pcss('a[href^=""]') == []
|
||||
assert pcss('a[href$="org"]') == ['nofollow-anchor']
|
||||
assert pcss('a[href$=""]') == []
|
||||
assert pcss('div[foobar~="bc"]', 'div[foobar~="cde"]') == [
|
||||
'foobar-div']
|
||||
assert pcss('[foobar~="ab bc"]',
|
||||
'[foobar~=""]', '[foobar~=" \t"]') == []
|
||||
assert pcss('div[foobar~="cd"]') == []
|
||||
assert pcss('*[lang|="En"]', '[lang|="En-us"]') == ['second-li']
|
||||
# Attribute values are case sensitive
|
||||
assert pcss('*[lang|="en"]', '[lang|="en-US"]') == []
|
||||
assert pcss('*[lang|="e"]') == []
|
||||
# ... :lang() is not.
|
||||
assert pcss(
|
||||
':lang(EN)', '*:lang(en-US)'
|
||||
':lang(En)'
|
||||
) == ['second-li', 'li-div']
|
||||
assert pcss(':lang(e)' # , html_only=True
|
||||
) == []
|
||||
assert pcss('li:nth-child(3)') == ['third-li']
|
||||
assert pcss('li:nth-child(10)') == []
|
||||
assert pcss('li:nth-child(2n)', 'li:nth-child(even)',
|
||||
'li:nth-child(2n+0)') == [
|
||||
'second-li', 'fourth-li', 'sixth-li']
|
||||
assert pcss('li:nth-child(+2n+1)', 'li:nth-child(odd)') == [
|
||||
'first-li', 'third-li', 'fifth-li', 'seventh-li']
|
||||
assert pcss('li:nth-child(2n+4)') == ['fourth-li', 'sixth-li']
|
||||
assert pcss('li:nth-child(3n+1)') == [
|
||||
'first-li', 'fourth-li', 'seventh-li']
|
||||
assert pcss('li:nth-last-child(1)') == ['seventh-li']
|
||||
assert pcss('li:nth-last-child(0)') == []
|
||||
assert pcss('li:nth-last-child(2n+2)', 'li:nth-last-child(even)') == [
|
||||
'second-li', 'fourth-li', 'sixth-li']
|
||||
assert pcss('li:nth-last-child(2n+4)') == ['second-li', 'fourth-li']
|
||||
assert pcss('ol:first-of-type') == ['first-ol']
|
||||
assert pcss('ol:nth-child(1)') == []
|
||||
assert pcss('ol:nth-of-type(2)') == ['second-ol']
|
||||
assert pcss('ol:nth-last-of-type(2)') == ['first-ol']
|
||||
assert pcss('span:only-child') == ['foobar-span']
|
||||
assert pcss('div:only-child') == ['li-div']
|
||||
assert pcss('div *:only-child') == ['li-div', 'foobar-span']
|
||||
assert pcss('p *:only-of-type') == ['p-em', 'fieldset']
|
||||
assert pcss('p:only-of-type') == ['paragraph']
|
||||
assert pcss('a:empty', 'a:EMpty') == ['name-anchor']
|
||||
assert pcss('li:empty') == [
|
||||
'third-li', 'fourth-li', 'fifth-li', 'sixth-li']
|
||||
assert pcss(':root', 'html:root') == ['html']
|
||||
assert pcss('li:root', '* :root') == []
|
||||
assert pcss('.a', '.b', '*.a', 'ol.a') == ['first-ol']
|
||||
assert pcss('.c', '*.c') == ['first-ol', 'third-li', 'fourth-li']
|
||||
assert pcss('ol *.c', 'ol li.c', 'li ~ li.c', 'ol > li.c') == [
|
||||
'third-li', 'fourth-li']
|
||||
assert pcss('#first-li', 'li#first-li', '*#first-li') == ['first-li']
|
||||
assert pcss('li div', 'li > div', 'div div') == ['li-div']
|
||||
assert pcss('div > div') == []
|
||||
assert pcss('div>.c', 'div > .c') == ['first-ol']
|
||||
assert pcss('div + div') == ['foobar-div']
|
||||
assert pcss('a ~ a') == ['tag-anchor', 'nofollow-anchor']
|
||||
assert pcss('a[rel="tag"] ~ a') == ['nofollow-anchor']
|
||||
assert pcss('ol#first-ol li:last-child') == ['seventh-li']
|
||||
assert pcss('ol#first-ol *:last-child') == ['li-div', 'seventh-li']
|
||||
assert pcss('#outer-div:first-child') == ['outer-div']
|
||||
assert pcss('#outer-div :first-child') == [
|
||||
'name-anchor', 'first-li', 'li-div', 'p-b',
|
||||
'checkbox-fieldset-disabled', 'area-href']
|
||||
assert pcss('a[href]') == ['tag-anchor', 'nofollow-anchor']
|
||||
assert pcss(':not(*)') == []
|
||||
assert pcss('a:not([href])') == ['name-anchor']
|
||||
assert pcss('ol :Not([class])') == [
|
||||
'first-li', 'second-li', 'li-div',
|
||||
'fifth-li', 'sixth-li', 'seventh-li']
|
||||
# Invalid characters in XPath element names, should not crash
|
||||
assert pcss(r'di\a0 v', r'div\[') == []
|
||||
assert pcss(r'[h\a0 ref]', r'[h\]ref]') == []
|
||||
|
||||
assert pcss(':link') == [
|
||||
'link-href', 'tag-anchor', 'nofollow-anchor', 'area-href']
|
||||
assert pcss('HTML :link', html_only=True) == [
|
||||
'link-href', 'tag-anchor', 'nofollow-anchor', 'area-href']
|
||||
assert pcss(':visited') == []
|
||||
assert pcss(':enabled') == [
|
||||
'link-href', 'tag-anchor', 'nofollow-anchor',
|
||||
'checkbox-unchecked', 'text-checked', 'input-hidden',
|
||||
'checkbox-checked', 'area-href']
|
||||
assert pcss(':disabled') == [
|
||||
'checkbox-disabled', 'input-hidden-disabled',
|
||||
'checkbox-disabled-checked', 'fieldset',
|
||||
'checkbox-fieldset-disabled',
|
||||
'hidden-fieldset-disabled']
|
||||
assert pcss(':checked') == [
|
||||
'checkbox-checked', 'checkbox-disabled-checked']
|
||||
|
||||
|
||||
def test_select_shakespeare():
|
||||
document = etree.fromstring(HTML_SHAKESPEARE)
|
||||
body = document.find('.//{http://www.w3.org/1999/xhtml}body')
|
||||
body = ElementWrapper.from_xml_root(body)
|
||||
|
||||
def count(selector):
|
||||
return sum(1 for _ in body.query_all(selector))
|
||||
|
||||
# Data borrowed from http://mootools.net/slickspeed/
|
||||
|
||||
# # Changed from original; probably because I'm only
|
||||
# # searching the body.
|
||||
# assert count('*') == 252
|
||||
assert count('*') == 246
|
||||
# assert count('div:contains(CELIA)') == 26
|
||||
assert count('div:only-child') == 22 # ?
|
||||
assert count('div:nth-child(even)') == 106
|
||||
assert count('div:nth-child(2n)') == 106
|
||||
assert count('div:nth-child(odd)') == 137
|
||||
assert count('div:nth-child(2n+1)') == 137
|
||||
assert count('div:nth-child(n)') == 243
|
||||
assert count('div:last-child') == 53
|
||||
assert count('div:first-child') == 51
|
||||
assert count('div > div') == 242
|
||||
assert count('div + div') == 190
|
||||
assert count('div ~ div') == 190
|
||||
assert count('body') == 1
|
||||
assert count('body div') == 243
|
||||
assert count('div') == 243
|
||||
assert count('div div') == 242
|
||||
assert count('div div div') == 241
|
||||
assert count('div, div, div') == 243
|
||||
assert count('div, a, span') == 243
|
||||
assert count('.dialog') == 51
|
||||
assert count('div.dialog') == 51
|
||||
assert count('div .dialog') == 51
|
||||
assert count('div.character, div.dialog') == 99
|
||||
assert count('div.direction.dialog') == 0
|
||||
assert count('div.dialog.direction') == 0
|
||||
assert count('div.dialog.scene') == 1
|
||||
assert count('div.scene.scene') == 1
|
||||
assert count('div.scene .scene') == 0
|
||||
assert count('div.direction .dialog ') == 0
|
||||
assert count('div .dialog .direction') == 4
|
||||
assert count('div.dialog .dialog .direction') == 4
|
||||
assert count('#speech5') == 1
|
||||
assert count('div#speech5') == 1
|
||||
assert count('div #speech5') == 1
|
||||
assert count('div.scene div.dialog') == 49
|
||||
assert count('div#scene1 div.dialog div') == 142
|
||||
assert count('#scene1 #speech1') == 1
|
||||
assert count('div[class]') == 103
|
||||
assert count('div[class=dialog]') == 50
|
||||
assert count('div[class^=dia]') == 51
|
||||
assert count('div[class$=log]') == 50
|
||||
assert count('div[class*=sce]') == 1
|
||||
assert count('div[class|=dialog]') == 50 # ? Seems right
|
||||
# assert count('div[class!=madeup]') == 243 # ? Seems right
|
||||
assert count('div[class~=dialog]') == 51 # ? Seems right
|
||||
|
||||
|
||||
HTML_IDS = open(resource('ids.html')).read()
|
||||
HTML_SHAKESPEARE = open(resource('shakespeare.html')).read()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,366 @@
|
|||
# coding: utf8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from webencodings import ascii_lower
|
||||
|
||||
from ._compat import basestring, ifilter
|
||||
from .compiler import compile_selector_list, split_whitespace
|
||||
|
||||
|
||||
class cached_property(object):
|
||||
# Borrowed from Werkzeug
|
||||
# https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/utils.py
|
||||
|
||||
def __init__(self, func, name=None, doc=None):
|
||||
self.__name__ = name or func.__name__
|
||||
self.__module__ = func.__module__
|
||||
self.__doc__ = doc or func.__doc__
|
||||
self.func = func
|
||||
|
||||
def __get__(self, obj, type=None, __missing=object()):
|
||||
if obj is None:
|
||||
return self
|
||||
value = obj.__dict__.get(self.__name__, __missing)
|
||||
if value is __missing:
|
||||
value = self.func(obj)
|
||||
obj.__dict__[self.__name__] = value
|
||||
return value
|
||||
|
||||
|
||||
class ElementWrapper(object):
|
||||
"""
|
||||
A wrapper for an ElementTree :class:`~xml.etree.ElementTree.Element`
|
||||
for Selector matching.
|
||||
|
||||
This class should not be instanciated directly.
|
||||
:meth:`from_xml_root` or :meth:`from_html_root` should be used
|
||||
for the root element of a document,
|
||||
and other elements should be accessed (and wrappers generated)
|
||||
using methods such as :meth:`iter_children` and :meth:`iter_subtree`.
|
||||
|
||||
:class:`ElementWrapper` objects compare equal
|
||||
if their underlying :class:`~xml.etree.ElementTree.Element` do.
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def from_xml_root(cls, root, content_language=None):
|
||||
"""Wrap for selector matching the root of an XML or XHTML document.
|
||||
|
||||
:param root:
|
||||
An ElementTree :class:`~xml.etree.ElementTree.Element`
|
||||
for the root element of a document.
|
||||
If the given element is not the root,
|
||||
selector matching will behave is if it were.
|
||||
In other words, selectors will be `scope-contained`_
|
||||
to the subtree rooted at that element.
|
||||
:returns:
|
||||
A new :class:`ElementWrapper`
|
||||
|
||||
.. _scope-contained:
|
||||
http://dev.w3.org/csswg/selectors4/#scope-contained-selectors
|
||||
|
||||
"""
|
||||
return cls._from_root(root, content_language, in_html_document=False)
|
||||
|
||||
@classmethod
|
||||
def from_html_root(cls, root, content_language=None):
|
||||
"""Same as :meth:`from_xml_root`,
|
||||
but for documents parsed with an HTML parser
|
||||
like `html5lib <http://html5lib.readthedocs.org/>`_,
|
||||
which should be the case of documents with the ``text/html`` MIME type.
|
||||
|
||||
Compared to :meth:`from_xml_root`,
|
||||
this makes element attribute names in Selectors case-insensitive.
|
||||
|
||||
"""
|
||||
return cls._from_root(root, content_language, in_html_document=True)
|
||||
|
||||
@classmethod
|
||||
def _from_root(cls, root, content_language, in_html_document=True):
|
||||
if hasattr(root, 'getroot'):
|
||||
root = root.getroot()
|
||||
return cls(root, parent=None, index=0, previous=None,
|
||||
in_html_document=in_html_document,
|
||||
content_language=content_language)
|
||||
|
||||
def __init__(self, etree_element, parent, index, previous,
|
||||
in_html_document, content_language=None):
|
||||
#: The underlying ElementTree :class:`~xml.etree.ElementTree.Element`
|
||||
self.etree_element = etree_element
|
||||
#: The parent :class:`ElementWrapper`,
|
||||
#: or :obj:`None` for the root element.
|
||||
self.parent = parent
|
||||
#: The previous sibling :class:`ElementWrapper`,
|
||||
#: or :obj:`None` for the root element.
|
||||
self.previous = previous
|
||||
if parent is not None:
|
||||
#: The :attr:`parent`’s children
|
||||
#: as a list of
|
||||
#: ElementTree :class:`~xml.etree.ElementTree.Element`s.
|
||||
#: For the root (which has no parent)
|
||||
self.etree_siblings = parent.etree_children
|
||||
else:
|
||||
self.etree_siblings = [etree_element]
|
||||
#: The position within the :attr:`parent`’s children, counting from 0.
|
||||
#: ``e.etree_siblings[e.index]`` is always ``e.etree_element``.
|
||||
self.index = index
|
||||
self.in_html_document = in_html_document
|
||||
self.transport_content_language = content_language
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(self) == type(other) and
|
||||
self.etree_element == other.etree_element)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((type(self), self.etree_element))
|
||||
|
||||
def __iter__(self):
|
||||
for element in self.iter_children():
|
||||
yield element
|
||||
|
||||
def iter_ancestors(self):
|
||||
"""Return an iterator of existing :class:`ElementWrapper` objects
|
||||
for this element’s ancestors,
|
||||
in reversed tree order (from :attr:`parent` to the root)
|
||||
|
||||
The element itself is not included,
|
||||
this is an empty sequence for the root element.
|
||||
|
||||
"""
|
||||
element = self
|
||||
while element.parent is not None:
|
||||
element = element.parent
|
||||
yield element
|
||||
|
||||
def iter_previous_siblings(self):
|
||||
"""Return an iterator of existing :class:`ElementWrapper` objects
|
||||
for this element’s previous siblings,
|
||||
in reversed tree order.
|
||||
|
||||
The element itself is not included,
|
||||
this is an empty sequence for a first child or the root element.
|
||||
|
||||
"""
|
||||
element = self
|
||||
while element.previous is not None:
|
||||
element = element.previous
|
||||
yield element
|
||||
|
||||
def iter_children(self):
|
||||
"""Return an iterator of newly-created :class:`ElementWrapper` objects
|
||||
for this element’s child elements,
|
||||
in tree order.
|
||||
|
||||
"""
|
||||
child = None
|
||||
for i, etree_child in enumerate(self.etree_children):
|
||||
child = type(self)(
|
||||
etree_child,
|
||||
parent=self,
|
||||
index=i,
|
||||
previous=child,
|
||||
in_html_document=self.in_html_document,
|
||||
)
|
||||
yield child
|
||||
|
||||
def iter_subtree(self):
|
||||
"""Return an iterator of newly-created :class:`ElementWrapper` objects
|
||||
for the entire subtree rooted at this element,
|
||||
in tree order.
|
||||
|
||||
Unlike in other methods, the element itself *is* included.
|
||||
|
||||
This loops over an entire document:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for element in ElementWrapper.from_root(root_etree).iter_subtree():
|
||||
...
|
||||
|
||||
"""
|
||||
stack = [iter([self])]
|
||||
while stack:
|
||||
element = next(stack[-1], None)
|
||||
if element is None:
|
||||
stack.pop()
|
||||
else:
|
||||
yield element
|
||||
stack.append(element.iter_children())
|
||||
|
||||
@staticmethod
|
||||
def _compile(selectors):
|
||||
return [
|
||||
compiled_selector.test
|
||||
for selector in selectors
|
||||
for compiled_selector in (
|
||||
[selector] if hasattr(selector, 'test')
|
||||
else compile_selector_list(selector)
|
||||
)
|
||||
if compiled_selector.pseudo_element is None and
|
||||
not compiled_selector.never_matches
|
||||
]
|
||||
|
||||
def matches(self, *selectors):
|
||||
"""Return wether this elememt matches any of the given selectors.
|
||||
|
||||
:param selectors:
|
||||
Each given selector is either a :class:`CompiledSelector`,
|
||||
or an argument to :func:`compile_selector_list`.
|
||||
|
||||
"""
|
||||
return any(test(self) for test in self._compile(selectors))
|
||||
|
||||
def query_all(self, *selectors):
|
||||
"""
|
||||
Return elements, in tree order, that match any of the given selectors.
|
||||
|
||||
Selectors are `scope-filtered`_ to the subtree rooted at this element.
|
||||
|
||||
.. _scope-filtered: http://dev.w3.org/csswg/selectors4/#scope-filtered
|
||||
|
||||
:param selectors:
|
||||
Each given selector is either a :class:`CompiledSelector`,
|
||||
or an argument to :func:`compile_selector_list`.
|
||||
:returns:
|
||||
An iterator of newly-created :class:`ElementWrapper` objects.
|
||||
|
||||
"""
|
||||
tests = self._compile(selectors)
|
||||
if len(tests) == 1:
|
||||
return ifilter(tests[0], self.iter_subtree())
|
||||
elif selectors:
|
||||
return (
|
||||
element
|
||||
for element in self.iter_subtree()
|
||||
if any(test(element) for test in tests)
|
||||
)
|
||||
else:
|
||||
return iter(())
|
||||
|
||||
def query(self, *selectors):
|
||||
"""Return the first element (in tree order)
|
||||
that matches any of the given selectors.
|
||||
|
||||
:param selectors:
|
||||
Each given selector is either a :class:`CompiledSelector`,
|
||||
or an argument to :func:`compile_selector_list`.
|
||||
:returns:
|
||||
A newly-created :class:`ElementWrapper` object,
|
||||
or :obj:`None` if there is no match.
|
||||
|
||||
"""
|
||||
return next(self.query_all(*selectors), None)
|
||||
|
||||
@cached_property
|
||||
def etree_children(self):
|
||||
"""This element’s children,
|
||||
as a list of ElementTree :class:`~xml.etree.ElementTree.Element`.
|
||||
|
||||
Other ElementTree nodes such as
|
||||
:class:`comments <~xml.etree.ElementTree.Comment>` and
|
||||
:class:`processing instructions
|
||||
<~xml.etree.ElementTree.ProcessingInstruction>`
|
||||
are not included.
|
||||
|
||||
"""
|
||||
return [c for c in self.etree_element if isinstance(c.tag, basestring)]
|
||||
|
||||
@cached_property
|
||||
def local_name(self):
|
||||
"""The local name of this element, as a string."""
|
||||
namespace_url, local_name = _split_etree_tag(self.etree_element.tag)
|
||||
self.__dict__[str('namespace_url')] = namespace_url
|
||||
return local_name
|
||||
|
||||
@cached_property
|
||||
def namespace_url(self):
|
||||
"""The namespace URL of this element, as a string."""
|
||||
namespace_url, local_name = _split_etree_tag(self.etree_element.tag)
|
||||
self.__dict__[str('local_name')] = local_name
|
||||
return namespace_url
|
||||
|
||||
@cached_property
|
||||
def id(self):
|
||||
"""The ID of this element, as a string."""
|
||||
return self.etree_element.get('id')
|
||||
|
||||
@cached_property
|
||||
def classes(self):
|
||||
"""The classes of this element, as a :class:`set` of strings."""
|
||||
return set(split_whitespace(self.etree_element.get('class', '')))
|
||||
|
||||
@cached_property
|
||||
def lang(self):
|
||||
"""The language of this element, as a string."""
|
||||
# http://whatwg.org/C#language
|
||||
xml_lang = self.etree_element.get(
|
||||
'{http://www.w3.org/XML/1998/namespace}lang')
|
||||
if xml_lang is not None:
|
||||
return ascii_lower(xml_lang)
|
||||
is_html = (
|
||||
self.in_html_document or
|
||||
self.namespace_url == 'http://www.w3.org/1999/xhtml')
|
||||
if is_html:
|
||||
lang = self.etree_element.get('lang')
|
||||
if lang is not None:
|
||||
return ascii_lower(lang)
|
||||
if self.parent is not None:
|
||||
return self.parent.lang
|
||||
# Root elememnt
|
||||
if is_html:
|
||||
content_language = None
|
||||
for meta in etree_iter(self.etree_element,
|
||||
'{http://www.w3.org/1999/xhtml}meta'):
|
||||
http_equiv = meta.get('http-equiv', '')
|
||||
if ascii_lower(http_equiv) == 'content-language':
|
||||
content_language = _parse_content_language(
|
||||
meta.get('content'))
|
||||
if content_language is not None:
|
||||
return ascii_lower(content_language)
|
||||
# Empty string means unknown
|
||||
return _parse_content_language(self.transport_content_language) or ''
|
||||
|
||||
@cached_property
|
||||
def in_disabled_fieldset(self):
|
||||
if self.parent is None:
|
||||
return False
|
||||
if (self.parent.etree_element.tag == (
|
||||
'{http://www.w3.org/1999/xhtml}fieldset') and
|
||||
self.parent.etree_element.get('disabled') is not None and (
|
||||
self.etree_element.tag != (
|
||||
'{http://www.w3.org/1999/xhtml}legend') or
|
||||
any(s.etree_element.tag == (
|
||||
'{http://www.w3.org/1999/xhtml}legend')
|
||||
for s in self.iter_previous_siblings()))):
|
||||
return True
|
||||
return self.parent.in_disabled_fieldset
|
||||
|
||||
|
||||
def _split_etree_tag(tag):
|
||||
pos = tag.rfind('}')
|
||||
if pos == -1:
|
||||
return '', tag
|
||||
else:
|
||||
assert tag[0] == '{'
|
||||
return tag[1:pos], tag[pos + 1:]
|
||||
|
||||
|
||||
if hasattr(etree.Element, 'iter'):
|
||||
def etree_iter(element, tag=None):
|
||||
return element.iter(tag)
|
||||
else:
|
||||
def etree_iter(element, tag=None):
|
||||
return element.getiterator(tag)
|
||||
|
||||
|
||||
def _parse_content_language(value):
|
||||
if value is not None and ',' not in value:
|
||||
parts = split_whitespace(value)
|
||||
if len(parts) == 1:
|
||||
return parts[0]
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# cssselect2 documentation build configuration file.
|
||||
|
||||
import codecs
|
||||
import re
|
||||
from os import path
|
||||
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'cssselect2'
|
||||
copyright = '2012-2017, Simon Sapin'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = re.search("VERSION = '([^']+)'", codecs.open(
|
||||
path.join(path.dirname(path.dirname(__file__)), 'cssselect2', '__init__.py'),
|
||||
encoding='utf-8',
|
||||
).read().strip()).group(1)
|
||||
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'cssselect2doc'
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'cssselect2', 'cssselect2 Documentation',
|
||||
['Simon Sapin'], 1)
|
||||
]
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'cssselect2', 'cssselect2 Documentation',
|
||||
'Simon Sapin', 'cssselect2', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'py2': ('http://docs.python.org/2', None),
|
||||
'py3': ('http://docs.python.org/3', None),
|
||||
'webencodings': ('http://pythonhosted.org/webencodings/', None)}
|
|
@ -0,0 +1,41 @@
|
|||
.. include:: ../README.rst
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Installing cssselect2 with pip_ should Just Work::
|
||||
|
||||
pip install cssselect2
|
||||
|
||||
This will also automatically install cssselect2’s only dependency, tinycss2_.
|
||||
cssselect2 and tinycss2 both only contain Python code and should work on any
|
||||
Python implementation, although they’re only tested on CPython.
|
||||
|
||||
.. _pip: http://pip-installer.org/
|
||||
.. _tinycss2: http://tinycss2.readthedocs.io/
|
||||
|
||||
|
||||
Basic Example
|
||||
=============
|
||||
|
||||
Here is a classical cssselect2 workflow:
|
||||
|
||||
- parse a CSS stylesheet using tinycss2_,
|
||||
- store the CSS rules in a :meth:`~cssselect2.Matcher` object,
|
||||
- parse an HTML document using an ElementTree-like parser,
|
||||
- wrap the HTML tree in a :meth:`~cssselect2.ElementWrapper` object,
|
||||
- find the CSS rules matching each HTML tag, using the matcher and the wrapper.
|
||||
|
||||
.. literalinclude:: ../example.py
|
||||
|
||||
.. module:: cssselect2
|
||||
.. autoclass:: Matcher
|
||||
:members:
|
||||
.. autofunction:: compile_selector_list
|
||||
.. autoclass:: ElementWrapper
|
||||
:members:
|
||||
.. autoclass:: SelectorError
|
||||
|
||||
|
||||
.. include:: ../CHANGES
|
|
@ -0,0 +1,49 @@
|
|||
from xml.etree import ElementTree
|
||||
|
||||
import cssselect2
|
||||
import tinycss2
|
||||
|
||||
|
||||
# Parse CSS and add rules to the matcher
|
||||
|
||||
matcher = cssselect2.Matcher()
|
||||
|
||||
rules = tinycss2.parse_stylesheet('''
|
||||
body { font-size: 2em }
|
||||
body p { background: red }
|
||||
p { color: blue }
|
||||
''', skip_whitespace=True)
|
||||
|
||||
for rule in rules:
|
||||
selectors = cssselect2.compile_selector_list(rule.prelude)
|
||||
selector_string = tinycss2.serialize(rule.prelude)
|
||||
content_string = tinycss2.serialize(rule.content)
|
||||
payload = (selector_string, content_string)
|
||||
for selector in selectors:
|
||||
matcher.add_selector(selector, payload)
|
||||
|
||||
|
||||
# Parse HTML and find CSS rules applying to each tag
|
||||
|
||||
html_tree = ElementTree.fromstring('''
|
||||
<html>
|
||||
<body>
|
||||
<p>Test</p>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
wrapper = cssselect2.ElementWrapper.from_html_root(html_tree)
|
||||
for element in wrapper.iter_subtree():
|
||||
tag = element.etree_element.tag.split('}')[-1]
|
||||
print('Found tag "{}" in HTML'.format(tag))
|
||||
|
||||
matches = matcher.match(element)
|
||||
if matches:
|
||||
for match in matches:
|
||||
specificity, order, pseudo, payload = match
|
||||
selector_string, content_string = payload
|
||||
print('Matching selector "{}" ({})'.format(
|
||||
selector_string, content_string))
|
||||
else:
|
||||
print('No rule matching this tag')
|
||||
print()
|
|
@ -0,0 +1,14 @@
|
|||
[aliases]
|
||||
test = pytest
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=cssselect2 --flake8 --isort cssselect2
|
||||
norecursedirs = dist .cache .git build *.egg-info .eggs venv
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf8
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
ROOT = os.path.dirname(__file__)
|
||||
README = open(os.path.join(ROOT, 'README.rst')).read()
|
||||
INIT_PY = open(os.path.join(ROOT, 'cssselect2', '__init__.py')).read()
|
||||
VERSION = re.search("VERSION = '([^']+)'", INIT_PY).group(1)
|
||||
|
||||
needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
|
||||
pytest_runner = ['pytest-runner'] if needs_pytest else []
|
||||
|
||||
setup(
|
||||
name='cssselect2',
|
||||
version=VERSION,
|
||||
author='Simon Sapin',
|
||||
author_email='simon.sapin@exyr.org',
|
||||
description='CSS selectors for Python ElementTree',
|
||||
long_description=README,
|
||||
url='http://packages.python.org/cssselect2/',
|
||||
license='BSD',
|
||||
packages=['cssselect2'],
|
||||
package_data={'cssselect2': ['tests/*']},
|
||||
install_requires=['tinycss2'],
|
||||
setup_requires=pytest_runner,
|
||||
test_suite='cssselect2.tests',
|
||||
tests_require=[
|
||||
'pytest-runner', 'pytest-cov', 'pytest-flake8', 'pytest-isort'],
|
||||
extras_require={'test': [
|
||||
'pytest-runner', 'pytest-cov', 'pytest-flake8', 'pytest-isort']},
|
||||
)
|
Loading…
Reference in New Issue