././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1717516292.8955424
cssutils-2.11.1/ 0000755 0001751 0000177 00000000000 14627634005 013061 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.coveragerc 0000644 0001751 0000177 00000000310 14627633762 015205 0 ustar 00runner docker [run]
omit =
# leading `*/` for pytest-dev/pytest-cov#456
*/.tox/*
disable_warnings =
couldnt-parse
[report]
show_missing = True
exclude_also =
# jaraco/skeleton#97
@overload
if TYPE_CHECKING:
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.editorconfig 0000644 0001751 0000177 00000000366 14627633762 015554 0 ustar 00runner docker root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 4
insert_final_newline = true
end_of_line = lf
[*.py]
indent_style = space
max_line_length = 88
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.rst]
indent_style = space
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1717516292.831542
cssutils-2.11.1/.github/ 0000755 0001751 0000177 00000000000 14627634005 014421 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.github/FUNDING.yml 0000644 0001751 0000177 00000000030 14627633762 016240 0 ustar 00runner docker tidelift: pypi/cssutils
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.github/dependabot.yml 0000644 0001751 0000177 00000000224 14627633762 017260 0 ustar 00runner docker version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-type: "all"
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1717516292.831542
cssutils-2.11.1/.github/workflows/ 0000755 0001751 0000177 00000000000 14627634005 016456 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.github/workflows/main.yml 0000644 0001751 0000177 00000005546 14627633762 020150 0 ustar 00runner docker name: tests
on:
merge_group:
push:
branches-ignore:
# temporary GH branches relating to merge queues (jaraco/skeleton#93)
- gh-readonly-queue/**
tags:
# required if branches-ignore is supplied (jaraco/skeleton#103)
- '**'
pull_request:
permissions:
contents: read
env:
# Environment variable to support color support (jaraco/skeleton#66)
FORCE_COLOR: 1
# Suppress noisy pip warnings
PIP_DISABLE_PIP_VERSION_CHECK: 'true'
PIP_NO_PYTHON_VERSION_WARNING: 'true'
PIP_NO_WARN_SCRIPT_LOCATION: 'true'
# Ensure tests can sense settings about the environment
TOX_OVERRIDE: >-
testenv.pass_env+=GITHUB_*,FORCE_COLOR
jobs:
test:
strategy:
# https://blog.jaraco.com/efficient-use-of-ci-resources/
matrix:
python:
- "3.8"
- "3.12"
platform:
- ubuntu-latest
- macos-latest
- windows-latest
include:
- python: "3.9"
platform: ubuntu-latest
- python: "3.10"
platform: ubuntu-latest
- python: "3.11"
platform: ubuntu-latest
- python: pypy3.10
platform: ubuntu-latest
runs-on: ${{ matrix.platform }}
continue-on-error: ${{ matrix.python == '3.13' }}
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install tox
run: python -m pip install tox
- name: Run
run: tox
collateral:
strategy:
fail-fast: false
matrix:
job:
- diffcov
- docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Install tox
run: python -m pip install tox
- name: Eval ${{ matrix.job }}
run: tox -e ${{ matrix.job }}
check: # This job does nothing and is only used for the branch protection
if: always()
needs:
- test
- collateral
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
release:
permissions:
contents: write
needs:
- check
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Install tox
run: python -m pip install tox
- name: Run
run: tox -e release
env:
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.pre-commit-config.yaml 0000644 0001751 0000177 00000000164 14627633762 017354 0 ustar 00runner docker repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.8
hooks:
- id: ruff
- id: ruff-format
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/.readthedocs.yaml 0000644 0001751 0000177 00000000516 14627633762 016323 0 ustar 00runner docker version: 2
python:
install:
- path: .
extra_requirements:
- doc
# required boilerplate readthedocs/readthedocs.org#10401
build:
os: ubuntu-lts-latest
tools:
python: latest
# post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114
jobs:
post_checkout:
- git fetch --unshallow || true
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/LICENSE 0000644 0001751 0000177 00000017172 14627633762 014107 0 ustar 00runner docker GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/NEWS.rst 0000644 0001751 0000177 00000360100 14627633762 014400 0 ustar 00runner docker v2.11.1
=======
Bugfixes
--------
- Consolidated license to simply LGPL 3. (#52)
v2.11.0
=======
Features
--------
- Reduced cyclomatic complexity in selector module. (#47)
v2.10.3
=======
Bugfixes
--------
- Fixed DeprecationWarning with cgi module.
v2.10.2
=======
Bugfixes
--------
- Remove optional dependency only required on unsupported Python versions. (#48)
v2.10.1
=======
Bugfixes
--------
- Replace xunit-style setup with pytest fixtures. (#45)
v2.10.0
=======
Features
--------
- getPropertyValue now allows specifying a default value. (#42)
v2.9.0
======
Features
--------
- Made URL fetcher lenient to missing metadata.
v2.8.0
======
Features
--------
- Require Python 3.8 or later.
Bugfixes
--------
- Made some unreachable tests reachable.
v2.7.1
======
#36: Refactored to restore docs builds without warning-errors.
v2.7.0
======
#35: Updated deprecated usage of ``cgi`` module.
#34: Updated deprecated setup/teardown from ``nose`` in tests.
Other miscellaneous cleanup and packaging updates.
v2.6.0
======
#14: Added support for custom CSS variables with ``--`` prefix.
v2.5.1
======
Some refactoring.
v2.5.0
======
Substantial code refresh. Ported tests to native pytest.
Enrolled project with Tidelift.
v2.4.2
======
#24: Added Python 3.11 compatibility to tests.
v2.4.1
======
#22: Removed ``cssutils.tests.basetest.get_resource_filename``.
#21: Cleaned up docs so they build without warnings.
v2.4.0
======
Require Python 3.7.
v2.3.1
======
Misc fixes in #10, #15, #17.
v2.3.0
======
#7: Add support for a ``calc()`` within a ``calc()``.
v2.2.0
======
Minor cleanup.
Restored package version reporting when fetching URLs.
v2.1.0
======
Adopted `jaraco/skeleton `_.
Delinted codebase.
Removed ``cssutils.VERSION`` and ``cssutils.__version__``.
Consumers wishing to know the version should use
``importlib.metadata.version('cssutils')``.
v2.0.0
======
New `maintainer `_ revives the project
and moves `hosting `_ with
continuous integration testing.
Refreshed packaging.
Project now requires Python 3.6 or later.
v1.0.2
======
Undocumented changes.
v1.0.1
======
Undocumented changes.
v1.0
====
(1.0 only cause I was tired of the 0.9.x releases ;)
- EXPERIMENTAL: Variable references may have a fallback value now (as implemented in Firefox 29). It is available as ``CSSVariable.fallback`` and example are::
bottom: var(b);
color: var(theme-colour-1, rgb(14,14,14));
left: var(L, 1px);
z-index: var(L, 1);
top: var(T, calc( 2 * 1px ));
background: var(U, url(example.png));
border-color: var(C, #f00)
- FEATURE: (Bitbucket #37) Implemented parsing of ``CSSCalc`` values. General syntax is checked but not if operators in calc are actually the right kind like DIMENSION * DIMENSION. Also Values using calc do not validate in cssutils but are actually valid.
- FIXED Bitbucket #20 and Bitbucket #35 (Test fail CSSParser.parseUrl() error with Python 3.3)
- FIXED Bitbucket #21: (almost all) deprecation warning in Py 3.3 fixed.
- FIXED Bitbucket #30 (Test failed)
- FIXED Bitbucket #33 (well kinda): Added that cssutils is **not** threadsafe!
- FIXED Bitbucket #34: More complext MediaQueries should be parsable now. A few slight changes in behavior are:
- ``xml.dom.SyntaxErr`` raised instead of ``xml.dom.InvalidCharacterErr`` for an unknown media type
- removed ``handheld`` media type special case (for old Opera).
v0.9.10
=======
- BUGFIX: False HASH color values like ``#xyz`` were not being handled properly (thanks to Teruaki Koizumi)
- Fixed Bitbucket #23: Tests do work properly in newer Python (>2.7.1?) version now, old versions will report error. Before this was the other way round but this makes more sense probably
- Fixed Bitbucket #24: rgba color values did not validate properly
- (Jason R. Coombs) Fixed Bitbucket #25: Cssutils installs "tests" package
- Fixed Bitbucket #27: Small magnitude float values serialized incorrectly
- Fixed Bitbucket #28: Standalone semicolons in CSSStyleDeclaration is now simply stripped and does not remove directly following property too
0.9.10b1 120428
- **REGRESSION**: Valid profiles reported by validation may not be right but as these hardly say anything in the real world anyway the advancements in the following bugfixes are probably worth this. ``Profiles.validateWithProfile`` currently not working but should be hardly used anyway. As a workaround remove all profiles and add just the ones you want to use for your application
- **BUGFIX**: Profiles have been updated. Multiple profiles can be added via ``addProfiles`` and this is preferred to adding separate profiles with ``addProfile``. Profiles do check for updated macros now so that e.g. CSS3 Colors defined in a different profile are valid for all properties defined before. This may lead to a reset of all properties and so may be an extremely expensive operation.
- BUGFIX: font-family names in profile CSS3_FONTS seem to have been defined wrongly. Family names containing spaces are valid now, even without quotes.
- BUGFIX: for Python 2.5 compatibility (thanks @Jason)
+ IMPROVEMENT: Added all X11 color names and corresponding RGBA values
v0.9.9
======
- FEATURE: Implemented API for ``MarginRule`` objects inside ``CSSPageRule``, see http://www.w3.org/TR/css3-page/. You can also use e.g. ``CSSPageRule['@top-left']`` to retrieve the MarginRule it it is set etc. All dict like methods should be there. If a margin is set twice or more all properties are merged into a single margin rule. Double set properties are all kept though (see below).
- FEATURE: ``parseStyle()`` has optional parameter ``validate=False`` now too to disable validation (default is always ``True``).
- FEATURE: ``CSSStyleDeclaration.setProperty`` has new option ``replace=True``. if True (DEFAULT) the given property will replace a present property. If False a new property will be added always. The difference to `normalize` is that two or more properties with the same name may be set, useful for e.g. stuff like::
background: red;
background: rgba(255, 0, 0, 0.5);
which defines the same property but only capable UAs use the last property value, older ones use the first value.
+ CHANGE: @rules attribute ``atkeyword`` value is now normalized. The actual keyword (example ``@IMPorT``) is kept and is optionally reserialized but in the example ``atkeyword == '@import'``
- BUGFIX: 'auto' is now an invalid CSSPageRule pagename.
- BUGFIX: Fixed issue for GoogleAppEngine (GAE) which somehow handles codecs differently. ``parseUrl`` should work now.
v0.9.8
======
0.9.8 final 111210
- FEATURE: Feature Request (Bitbucket #4) to be able to disable validation of a stylesheet has been implemented. Add Parameter ``validate=False`` for parsing.
+ BUGFIX: Fixed Bitbucket #5 Unicode escaping inside strings. Thanks to Simon Sapin
+ BUGFIX: The integer is optional in counter-reset and counter-increment, and not only on the first counter. Thanks to Simon Sapin
+ BUGFIX: Fix for unicode replacements by Denis Bilenko, thanks! https://bitbucket.org/cthedot/cssutils/pull-request/1/fix-a-bug-in-regex-which-accidentally
- IMPROVEMENT: ``parseStyle`` moved to CSSParser, thanks to Simon Sapin
0.9.8a3 110727
+ BUGFIX: Fixed validation of ``size`` property (thanks to Simon Sapin)
+ BUGFIX: Fixed Issue Bitbucket #55 (thanks to Simon Sapin): `outline-color` property was missing from validation.
+ BUGFIX: Fixed resolution of encoding detection of a stylesheet which did not use @charset in certain circumstances (mainly when imported sheets use different encoding than importing one which should be quite rare actually).
- FEATURE: Added ``URIValue.absoluteUri`` (thanks to Simon Sapin)
- FEATURE: Issue Bitbucket #53 feature request: Added new Preference option ``cssutils.ser.prefs.indentClosingBrace``. Defines if closing brace of block is indented to match indentation of the block (default) oder match indentation of selector.
- FEATURE: Feature request: Added new Preference option ``cssutils.ser.prefs.omitLeadingZero``. Defines if values between -1 and 1 should omit the 0, like ``.5px``. Minified settings do this, else 0 is kept by default.
+ CHANGE (minor): Some error messages have slightly changed due to a simpler compatibility to Python 3. Problem are any ``u'...'`` texts inside error messages which now are simplified, some without and quotes. Changed are e.g. error messages by ``Property``.
- **IMPROVEMENT**: Python 3 support. At least the unittests run in Python 2.5, 2.6, 2.7, 3.2 and Jython 2.5.1 now. Both encutils (with support by Fredrik Hedman, thanks!) and cssutils (thanks to Jaraco) and the CSS codec (thanks to Walter Dörwald) seem to work with Python 3 (tested on Python 3.2.1 Win64). Tests use Mock instead of MiniMock now as former is available for Python 2.x and 3.x.
- **IMPROVEMENT**: Parsing of longer (and probably invalid) ``font`` or ``font-family`` values was *extremely* slow due to a very complex regex. This has been changed and parsing of specific stylesheets using these values should be much faster now. (``macros[Profiles.CSS_LEVEL_2]['font-family']`` is gone so if you used this in your own validation modules you need to check the source in `profiles.py`.)
- IMPROVEMENT: Fixed Issue Bitbucket #54 (thanks to Simon Sapin): Short hand like `#f80` color value object have correct red, green and blue property values now. Also ``hsl()`` and ``hsla()`` colors report (almost) correct values (due to rounding problems).
- **Source control has moved to bitbucket https://bitbucket.org/cthedot/cssutils**. Older Issues are currently still at Google Code, newer at Bitbucket. Please do not use Google Code for new issue reports anymore!
0.9.8a2 110611
- BUGFIX: Fixed Issue Bitbucket #59 which showed a rather strange problem with longer space separated lists of font-family values being so slow to actually stop parsing.
- BUGFIX/IMPROVEMENT: Fixed Issue Bitbucket #48. ``CSSParser.parseUrl()`` uses the defined fetcher of this parser *for the initial stylesheet* at url too and not just the imported sheets *from* this sheet.
- BUGFIX: Fixed Issue Bitbucket #50 which prevented cssutils parsing the acid2.css file correctly. Problem were selectors starting directly with ``[class]`` (an attribute selector).
+ **API CHANGE (major)**
(Known) named colors are parsed as ColorValue objects now. These are the 16 simple colors (black, white, etc) and `transparent` but not all Extended color keywords yet. Also changed ``ColorValue.type`` to ``Value.COLOR_VALUE``. ColorValue has additional properties ``red, green, blue, alpha`` and ``colorType`` which is one of IDENT, HASH or FUNCTION for now.
+ API CHANGE (minor)
Removed already DEPRECATED ``cssutils.parse`` and ``CSSParser.parse``. Use the more specific functions/methods ``parseFile parseString parseUrl`` instead.
Removed already DEPRECATED ``cssutils.log.setlog`` and ``.setloglevel``. Use ``.setLog`` and ``.setLevel`` instead.
Removed already DEPRECATED ``cssutils.ser.keepUnkownAtRules`` (note the typo). Use ``.keepUnknownAtRules`` instead.
- IMPROVEMENT: Added validation profiles for some properties from `CSS Backgrounds and Borders Module Level 3 `__, `CSS3 Basic User Interface Module `__, `CSS Text Level 3 `__
mainly `cursor`, `outline`, `resize`, `box-shadow`, `text-shadow`
0.9.8a1 101212
+ **API CHANGE (major)**
replace CSSValue with PropertyValue, Value and other classes.
NEW CLASSES:
:class:`cssutils.css.PropertyValue`
replaces CSSValue and CSSValueList
- is iterable (iterates over all single Value objects which in soruce CSS might be separated by "," "/" or " "
- a comma separated list of IDENT values is no longer handled as a single String (e.g. ``Arial, sans-serif``)
:class:`cssutils.css.Value`
replaces CSSPrimitiveValue with separate ``value`` and ``type`` info (value is typed, so e.g. string for e.g. STRING, IDENT or URI values, int or float) and is base class for more specific values like:
:class:`cssutils.css.URIValue`
replaces CSSPrimitiveValue, additional attribute ``uri``
:class:`cssutils.css.DimensionValue`
replaces CSSPrimitiveValue, additional attribute ``dimension``
:class:`cssutils.css.ColorValue`
replaces CSSPrimitiveValue, additional attribute ``red``, ``green``, ``blue`` and ``alpha``
**TODO: Not yet complete, only rgb, rgba, hsl, hsla and has values use this object and color and alpha information no done yet!**
:class:`cssutils.css.CSSFunction`
replaces CSSPrimitiveValue function, not complete yet
also renamed ``ExpressionValue`` to :class:`cssutils.css.MSValue` with new API
- IMPROVEMENT/CHANGE: Validation of color values is tighter now. Values like ``hsl(1, 2, 3)`` do not validate as it must be ``hsl(1, 2%, 3%)``. This mostly effects HSL/A and RGB/A notation.
- **IMPROVEMENT**: New Value parsing and API accelerate parsing of style declarations which take about 20-30% less time now. Of course this depends on the complexity of your styles.
+ BUGFIX: fixes Bitbucket #41, Bitbucket #42, Bitbucket #45, Bitbucket #46
PropertyValue.value returns value without any comments now, else use PropertyValue.cssText
- FEATURE: ``cssutils.replaceUrls()`` accepts as first argument a `cssutils.css.CSSStyleSheet` but now also a
:class:`cssutils.css.CSSStyleDeclaration` object, so may be used like the following which is useful when you work with HTML style attributes::
>>> style = cssutils.parseStyle("background-image: url(1.png), url('2.png')")
>>> cssutils.replaceUrls(style, lambda url: 'prefix/'+url)
>>> print style.cssText
background-image: url(prefix/1.png), url(prefix/2.png)
(I omitted the validation error message as more than one background-image is not yet defined in the cssutils validator but does parse through without problems)
+ CHANGE: explicit `+` of any dimension, percentage of number value is kept now instead of being stripped as if put explicitly in the author SHOULD have meant something ;)
v0.9.7
======
+ **FUTURE CHANGE**: CSSValue and subclasses will most certain not be supported in the next cssutils 0.9.8 version. A simpler and hopefully more robust API is in development. So the next version will have incompatible changes so you might wanna use 0.9.8 from the start if you do anything fancy with CSSValue and related classes.
0.9.7b4 101101
+ *EXPERIMENTAL*: CSS function values using ``calc(...)`` should be partly parsable now (as experimental ExpressionValues currently)
- BUGFIX: MS specific values are parsed a bit more reliable if parsing of these values is activated (they probable are syntactically invalid!). E.g. ``top: expression(eval(document.documentElement.scrollTop))`` and also a few values for the MS specific ``filter`` property are parsed and reserialized now.
+ IMPROVEMENT: ``CSSStyleSheet.variables`` now contains all available variable values (from all imported sheets as well as in sheet defined itself)
0.9.7b3 100620
+ API CHANGE: Changed parameters of script/utility function ``csscombine``.
- parameter ``resolveVariables=True`` now (default was ``False`` before)
- ``minify = True`` will not parse Comments at all. This is not really a change as comments were not kept in a minified stylesheet anyway but this may speed up stylesheet combination a bit
+ **PERFORMANCE/IMPROVEMENT**: Added parameter ``parseComments=True`` to CSSParser. If parsing with ``parser = cssutils.CSSParser(parseComments=False).parse...`` comments in a given stylesheet are simple omitted from the resulting stylesheet DOM.
+ **PERFORMANCE**: Compiled productions in cssutils tokenizer are cached now (to clear it use ``cssutils.tokenize2._TOKENIZER_CACHE.clear()``) which results in a slight performance improvement. Thanks to Amit Moscovich!
0.9.7b2 100606
+ IMPROVEMENT/BUGFIX: CSSFunction value parameters may contain HASH values like ``#fff`` now. These are used in experimental properties like ``-moz-linear-gradient(top,#fff,#fff 55%,#e4e4e4)``. Fixes Bitbucket #38.
+ API CHANGE: ``cssutils.ser.prefs.resolveVariables == True`` is the default from 0.9.7b2 as CSSVariables are not in any official specification yet and better reflects what you probably want after serializing a stylesheet...
0.9.7b1 100530
+ **API CHANGE**: Child objects like the ``cssRules`` of a ``CSSStyleSheet`` or ``CSSMediaRule`` are no longer kept after resetting the complete contents of an object (setting ``cssText``). This should not be expected anyway but if you relied on something like the following please beware::
sheet = cssutils.parseString('a { color: red}')
initial_rules = sheet.cssRules
sheet.cssText = 'b { color: green}'
# true until 0.9.6a6: assert sheet.cssRules == initial_rules, but now:
assert sheet.cssRules != initial_rules
+ **IMPROVEMENT**: Massive speed improvement of handling of CSSVariables of a stylesheet which due to naive implementation was unbelievable slow when using a lot of vars... Should now scale a lot better, about factor 5-20 depending of amount of variables used.
+ IMPROVEMENT: Fair amount of refactoring resulting in a bit speed improvement generally too
+ CHANGE: If a CSS variable should be resolved (``cssutils.ser.prefs.resolveVariables == true``) but no value can be found a WARNING is logged now. Should be an ERROR actually but as currently lots of "fake" errors are reported would probably hurt more than help. A future release might improve this.
+ BUGFIX: Syntax of value of CSS Fonts Module Level 3 ``src`` property now validates if local font name is given with a quoted name, e.g.: ``src: local('Yanone Kaffeesatz')``
0.9.7a6 100523
+ **API CHANGE (major)**: When setting an objects ``cssText`` (or ``selectorText`` etc) property the underlying object is replaced with a new one now. E.g. if setting ``cssutils.css.CSSStyleRule.selectorText`` the underlying ``cssutils.css.CSSStyleRule.selectorList`` object is swapped to a new ``SelectorList`` object. This should be expected but cssutils until now kept the exact same object and changed its content *in-place*. Please be aware! (Also the strange ``_absorb`` method of some objects is gone which was used for this.)
+ **API CHANGE (minor)**: Renamed ``cssutils.ser.prefs.keepUnkownAtRules`` to ``cssutils.ser.prefs.keepUnknownAtRules`` due to misspelling, see Issue Bitbucket #37. A DeprecationWarning is issued on use.
+ API CHANGES (minor):
- ``cssutils.css.CSSImportRule.media`` and ``cssutils.css.CSSMediaRule.media`` are now writable (setting with a string or ``cssutils.stylesheets.MediaList``)
- msg level when setting ``cssutils.stylesheets.MediaList.appendMedium`` changed to INFO (was WARNING)
- ``str(cssutils.css.CSSStyleRule)`` slightly changed
- **IMPROVEMENT/BUGFIX**: Improved distribution: Egg release should no longer include the tests package, source release still should. Also added dependency package for tests (minimock) and removed documenation txt files from distribution (HTML still included of course). This also fixes Issue Bitbucket #36.
- IMPROVEMENT: cssutils issues a warning if a page selector is not one of the defined in the spec (``:first``, ``:left``, ``:right``).
- IMPROVEMENT: Refactored quite a lot and added a few tests for variables
0.9.7a5 100523
- same changes as 0.9.7a6 but release was incomplete :(
0.9.7a4 100323
- **API CHANGE**: ``CSSRule.NAMESPACE_RULE`` actual value has been changed from 8 to 10 (according to the change in the CSSOM spec). The actual integer values **SHOULD NOT** be used anyway! **Please do always use the ``CSSRule`` constants which are present in ALL CSSRule and subclass objects like CSSStyleRule, CSSImportRule etc.!**
- **API CHANGE**: ``CSSStyleSheet.setSerializer`` and ``CSSStyleSheet.setSerializerPref`` have been DEPRECATED. Use ``cssutils.setSerializer(serializer)`` or set pref in ``cssutils.ser.prefs`` instead.
- **IMPROVEMENT/CHANGE**: Did some minor changes to improve compliance to http://dev.w3.org/csswg/cssom
+ **MAJOR**: :class:`cssutils.css.CSSImportRule.styleSheet` always references a CSSStyleSheet now. ``parentStyleSheet`` of this sheet is ``None`` now
+ MINOR: added property ``alternate`` to :class:`cssutils.stylesheets.StyleSheet`, which for now is always ``False``
+ improved handling of encodings during imports (edge cases hopefully not found in the wild ;)
+ **FEATURE**: Started experimental implementation of `CSS Variables `_
**experimental and incomplete**
Related details:
- added ``cssutils.css.CSSStyleSheet.variables`` which is a :class:`cssutils.css.CSSVariablesDeclaration` containing all available variables in this CSSStyleSheet including the ones defined in imported sheets.
- ``cssutils.ser.prefs.resolveVariables == False``: If set to ``True`` tries to resolve all variable references and removes any CSSVariablesRules.
- ``cssutils.ser.prefs.normalizedVarNames==True``: Defines if variable names should be serialized normalized (they are used as being normalized anyway)
+ FEATURE: Added new options to ``cssutils.script.csscombine``:
+ ``cssText=None`` and ``href=None`` to start combination from a css string, which normally needs ``href`` to be able to resolve any imports.
+ ``resolveVariables=False`` which resolves any variables if given the value ``True``
+ DOCUMENTATION: Reordered and cleared docs up a bit
0.9.7a3 100314
- **API CHANGE**: ``CSSRule.NAMESPACE_RULE`` actual value has been changed from 7 to 8 (according to the change in the spec). Also ``CSSRule.COMMENT`` has a value of ``1001`` (was ``-1``) and ``CSSRule.VARIABLES_RULE`` has a value of ``1008`` (was ``8``) now (being not in the official spec yet anyway but values are now in the open valuespace above 1000). The actual integer values **SHOULD NOT** be used anyway! **Please do always use the ``CSSRule`` constants which are present in ALL CSSRule and subclass objects like CSSStyleRule, CSSImportRule etc.!**
+ FEATURE: Added ``CSSRuleList.rulesOfType(type)`` which return and iterator the rules of the given type only. May be used on both ``CSSStyleSheet.cssRules`` or ``CSSMediaRule.cssRules``. ``type`` is one of the constants defined in ``css.CSSRule`` like e.g. ``css.CSSRule.STYLE_RULE``.
+ FEATURE (strange): IE specific values like ``DropShadow(color=#eee, offX=2, offY=1)`` (and ``Blur``, ``Shadow``) *should* be parsed and serialized now as an ``Expression`` value. I have not tested this deeply and there may be problems but for some common cases theses values should at least survive a parse/serialize with cssutils.
- **BUGFIX/IMPROVEMENT**: Parser now handles FUNCTION values which themselves contain another FUNCTION as used by PrinceXML CSS like e.g. ``prince-link: target-counter(attr(href), page)``
0.9.7a2 091230
- **API CHANGE**: Setting a style declarations' property to ``None`` or the empty string effectively removes this property from the declaration. See also Issue Bitbucket #32.
+ **BUGFIX/FEATURE**: Fixed Issue 33: URL references (like ``url()`` values) in combined sheets are now adjusted even if sheets are not in the same folder. Only relative paths are adjusted.
- **BUGFIX**: Fixed parsing of FUNCTIONS in CSSUnknownRule like ``@bottom { counter(page) }`` which raised a false error of a mismatch of parenthesis
+ FEATURE: Added parameter ``ignoreImportRules=False`` to ``cssutils.replaceUrls`` which when set to ``True`` no longer reports URLs from @import rules but property values only (see Issue Bitbucket #33)
0.9.7a1
- test release only
0.9.7a0
- **API CHANGE**: Replaced init parameter and attribute ``css.Selector.parentList`` with ``css.Selector.parent``
- API CHANGE (minor): Removed ``css.Property.parentStyle`` which was deprecated for some times now in favor of ``css.Property.parent``
+ **IMPROVEMENT**: Massive speed improvement due to changes in internal parsing.
When tried in a real world situation (parsing the stylesheet for my own site inside a simple WSGI based CSS handler) the parser uses ~0.7-0.8s when using cssutils 0.9.6. With cssutils 0.9.7a0 it only needs ~0.21s so only about 1/3 to 1/4 the time...
+ FEATURE: Parameter `index` of ``CSSStyleSheet.deleteRule(index)`` and ``CSSMediaRule.deleteRule(index)`` may now also be a rule object to be removed from the contained cssRules list.
- INTERNAL: Added ``tokenizer.push()``. Prodparser uses a global tokenizer now.
v0.9.6
======
0.9.6 091007
+ BUGFIX: Definition of macro for `positivenum` in cssutils profiles actually did allow nagative numbers, fixed (thanks to Jason R. Coombs)
- distribution build with `distribute `_ instead of setuptools
0.9.6b5 090830
+ BUGFIX: Issue Bitbucket #30 fixed. Setup from source did not work.
0.9.6b4 090829
+ BUGFIX: Issue Bitbucket #29 fixed. Double defined namespaces are replaced with a single (the last one) now.
- IMPROVEMENT: ``cssutils.resolveImports`` now keeps media information when to be resolved @import rule uses these. It wraps the imported rules in an @media rule which uses the same media information from the @media rule in the original sheet.
An xml.dom.HierarchyRequestErr may occur if an imported sheet itself contains @imports with media information or other rules which are not allowed in a @media rule like @namespace rules. In that case cssutils cannot resolve the @import rule and logs a WARNING but keeps the original @import.
0.9.6b3 090802
+ **FEATURE**: Added parsing support and new profile for details defined in module Fonts http://www.w3.org/TR/css3-fonts/
+ new properties: ``font-stretch``, ``font-size-adjust``
+ @font-face rules use a different profile for validation now which uses the defined properties only:
- ``font-family font-style font-weight`` properties redefined
- added ``src`` and ``unicode-range`` properties
+ Added ``CSSFontFaceRule.valid``. A @font-face rule is valid if all font descriptions properties are valid and properties ``font-family`` and ``src`` are set.
+ **FEATURE**: Added ``cssutils.parseStyle(cssText, encoding='utf-8')`` convienience function which assumes that the given `cssText` is the content of an HTML style attribute. It returns a :class:`~cssutils.css.CSSStyleDeclaration`.
+ **FEATURE** (experimental, request from Bitbucket #27): Added ``css.CSSStyleDeclaration.children()`` which is a generator yielding any known children of a declaration including *all* properties, comments or CSSUnknownRules.
+ FEATURE: ``CSSStyleDeclaration.insertRule`` also accepts a ``CSSRuleList`` now (same as ``CSSStyleSheet`` which does this for some time now).
+ FEATURE: Added ``CSSStyleDeclaration.keys()`` method which analoguous to standard dict returns property names which are set in the declaration.
- **API CHANGE**: Replaced attribute ``css.Property.parentStyle`` with ``css.Property.parent`` (``parentStyle`` is DEPRECATED now).
- API CHANGE: Added attribute ``parent`` to all CSSRules. It contains the Parent Node of this CSSRule (currently if a CSSStyleDeclaration only!) or None.
- API CHANGE (minor): Removed parameter ``profiles`` from ``cssutils.css.Property.validate()``. During validation each property checks which profiles to use which normally are all registered profiles in cssutils.profile. Exceptions are @font-face (TODO: and @page) rules which use their specific profile only. To add custom properties or values for the validation of these rules you need to add these to ``properties[Profiles.CSS3_FONT_FACE]`` in module ``cssutils.profiles`` and reregister that profile.
+ **BUGFIX**: Improved child and parent node referencing.
- setting ``CSSStyleSheet.cssRules`` and ``CSSMediaRule.cssRules`` correctly update ``parentStyleSheet`` (and ``parentRule``) of contained rules now. Also settings ``cssRules`` should now work as expected.
- setting ``css.CSSStyleRule.selectorList`` with a ``css.SelectorList`` object uses this new object directly
- setting ``css.CSSStyleRule.style`` with a ``css.CSSStyleDeclaration`` object uses this new object directly
- ``CSSStyleDeclaration.parentRule`` was not properly set for CSSStyleRule, CSSPageRule and CSSFontFaceRule.
+ **BUGFIX**: Parsing of CSSValues with unknown function names with a specific length of 4 or 7 chars were resulting in a SyntaxErr. Also parsing of comma separated list of CSS FUNCTION values works now.
+ BUGFIX: Fixed validation problems:
- ``font-family: a b`` (values with spaces in names without being quoted) are parsed now without emitting an ERROR. These are indeed valid but discouraged and you should use quotes (more than one space is compacted to a single space anyway so rather complicated without quotes)
- negative lengths for the ``font-size`` property are now properly reported as ERRORs
- IMPROVEMENT (minor): cssutils sets the HTTP header ``User-Agent`` now when fetching sheets over HTTP (with e.g. ``cssutils.parseUrl``).
+ CHANGES:
- Refactored predefined ``Prod`` objects used for parsing of ``CSSValue``. Also added ``Predef.unicode_range`` and renamed ``CHAR`` to ``char``.
- Removed css3productions which were only used for tests only anyway and which were not up to date at all
- *FEATURE* (experimental): Added support to at least parse sheets with Microsoft only property values for ``filter`` which start with ``progid:DXImageTransform.Microsoft.[...](``. To enable these you need to set::
>>> from cssutils import settings
>>> settings.set('DXImageTransform.Microsoft', True)
>>> cssutils.ser.prefs.useMinified()
>>> text = 'a {filter: progid:DXImageTransform.Microsoft.BasicImage( rotation = 90 )}'
>>> print cssutils.parseString(text).cssText
a{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=90)}
>>>
This currently is a **major hack** but if you like to minimize sheets in the wild which use this kind of CSS cssutils at least can parse and reserialize them.
Also you cannot reset this change until you restart your program.
These custom CSS FUNCTION names are not normalized at all. Also stuff like ``expression(...)`` which was normalized until now is not anymore.
0.9.6b2
NO RELEASE
0.9.6b1 090609
+ BUGFIX: Fixed ``CSSPageRule.selectorText = ''`` which does reset the selector now
+ BUGFIX (minor): Removed false references in a few modules' ``__all__`` list
- IMPROVEMENT: Jython 2.5 (from RC4) runs all tests now but a few changes had to be done:
- exception messages sometimes do have a different format in Jython and Python (but also between Python versions >= 2.5 and < 2.4 ...)
- Jython's ``xml.dom.Exception`` misses attribute ``args`` it seems
0.9.6a4 090509
- **API CHANGE**: Reverted handling of exceptions (Bitbucket #24) as this did not work with PyXML installed. You may again use ``str(e)`` on any raised xml.dom.Exception ``e``. Since 0.9.6a0 exceptions raised did raise a tuple of message, line and col information. Now the message alone is raised (again). Line and col information is still available as ``e.line, e.col``.
+ BUGFIX: Fixed Bitbucket #22 parsing or actually reserializing of values like ``content: "\\"``
+ BUGFIX: All examples at http://www.w3.org/TR/2009/CR-CSS2-20090423/syndata.html#illegalvalues should work now as expected:
- Unknown ATKEYWORD tokens in selectors make these invalid now, see example : ``p @here {color: red}``
- completion of incomplete @media rule like ``@media screen { p:before { content: 'Hello`` does work now when parsing with a non raising parser
- **FEATURE**: Updated some parts to http://www.w3.org/TR/2009/CR-CSS2-20090423/changes.html#new (most changes decribed there were already done in cssutils)
+ updated tokenizer macro ``escape``
+ replaced media ``aural`` with ``speech``
+ property ``content`` has allowed value ``none`` now
+ property ``background-position`` allows mixed values like ``left 10%`` now
- **FEATURE**: New preference option ``keepUnkownAtRules = False`` which defines if unknown atrules like e.g. ``@three-dee {...}`` are kept or not. Setting this pref to ``False`` in result removes unknown @rules from the serialized sheet which is the default for the minified settings.
- **IMPROVEMENT**: Fixed Bitbucket #23. The examples/style.py example renderer was reusing ``Property`` objects for each HTML element so they effectively overwrote each other.
+ DOCUMENTATION: Using Sphinx 0.6.1 now
0.9.6a3 090426
- **IMPROVEMENT**: Fixed ``CSSStyleDeclaration.length`` for Python 2.6.2 (due to a backport from Python 2.7 to 2.6.2 the reversed() iterator has no __length__ anymore which cssutils was using)
+ **BUGFIX**: New version of encutils (0.9) with these fixes:
- invalid HTML (like ``< />``) does not stop the encoding detection anymore
- fixed ``tryEncodings`` if chardet is not installed
- mismatch is ``False`` now if mime-type is ``text/xml`` (or similar) and XML encoding pseudo attribute defines encoding as this is ignored completely!
- default encoding for CSS is UTF-8 now if not other HTTP info is given. @charset encoding information is **not** used by encutils!
- log output for mismatch uses ``!=`` instead of ``<>`` now
- fixed testcases which were not all tested :(most embarrassing)
+ **BUGFIX**: Fixed Bitbucket #21 http://code.google.com/p/cssutils/issues/detail?id=21. Definition of valid values for property `background-position` was wrong. Still mixed values like ``background-position: 0 top`` are invalid although most browsers accept them. But the CSS 2.1 spec defines it the above way. CSS3 backgrounds is not implemented yet in cssutils.
0.9.6a2 090308
+ **API CHANGE**: :class:`cssutils.profiles.Profiles` (introduced in 0.9.6a1) has been refactored:
- ``cssutils.profile`` (a ``cssutils.profiles.Profiles`` object) is now preset and available used for all validation
- moved variable ``cssutils.profiles.defaultprofile`` to attribute ``Profiles.defaultProfiles`` (and so also available as ``cssutils.profile.defaultProfiles``)
- renamed ``Profiles.CSS_BOX_LEVEL_3`` to ``Profiles.CSS3_BOX`` and ``Profiles.CSS_COLOR_LEVEL_3`` to ``Profiles.CSS3_COLOR``
- renamed ``Profiles.basicmacros`` to ``Profiles._TOKEN_MACROS`` and ``Profiles.generalmacros`` to ``Profiles._MACROS``. As these two are always added to your property definitions there is no need to use these predefined macro dictionaries in your code.
- renamed ``Profiles.knownnames`` to ``Profiles.knownNames``
- ``Profiles.validateWithProfile`` returns ``valid, matching, profiles`` now
- renamed named parameter in ``cssutils.css.Property.validate(profiles=None)`` from ``profile`` to ``profiles``
- ``cssutils.profiles.properties`` (and new ``cssutils.profiles.macros``) use as keys the predefined constants in Profiles, like e.g. ``Profiles.CSS_LEVEL_2`` now. If you want to use some of the predefind macros you may e.g. use ``cssutils.profiles.macros[Profiles.CSS_LEVEL_2]['family-name']`` (in addition to the always available ``Profiles._TOKEN_MACROS`` and ``Profiles._MACROS``).
+ **CHANGE**: Reporting levels of properties have changed. Please see :meth:`cssutils.css.Property.validate` for details. E.g. valid properties in the current profile are only reported on DEBUG and not INFO level anymore. The log output has been changed too, context information is provided now (line, column and name of the relevant property)
- FEATURE: Added new properties as profile ``Profiles.CSS3_PAGED_MEDIA``: *fit*, *fit-position*, *image-orientation*, *page*, *size* and relevant properties already defined in ``Profiles.CSS_LEVEL_2``
+ **BUGFIX**: ``p.valid == False`` is now set for Properties not valid in the current profile even if they are valid in a different profile
+ **BUGFIX**: Macros used when adding a new profile to ``cssutils.profile`` were polluted with ALL macros ever defined. They now correctly use the (always used) predefined macros ``Profiles._TOKEN_MACROS`` and ``PROFILES._MACROS`` in addition to the given macros (``cssutils.profile.addProfile(self, profile, properties, macros=None)``) only. If you want to use any macros defined in other profiles you need to add these to your own macros as seen in ``macros[Profiles.CSS3_BOX]``.
+ BUGFIX: If ``cssutils.ser.prefs.validOnly == True`` serializer incorrectly put a single ``;`` for invalid properties out until now.
- **IMPROVEMENT**: comments added by ``cssutils.resolveImports`` only use the @import rules' href and not the absolute href of the referenced sheets anymore (might have been a possible security hole when showing a full local path to a sheet in a combined but not minified sheet)
- IMPROVEMENT: IE specific `alpha` values like ``filter: alpha(opacity=80)`` are parsed and kept now.
0.9.6a1 090207
- **API CHANGE**: Known but invalid properties raise/log an ERROR instead of a WARNING now. Properties not expected in the current profile log an INFO. As the default profile is ``None`` even basic properties like ``color`` are logged now. You may want to change the default profile by setting e.g. ``cssutils.profiles.defaultprofile = cssutils.profiles.Profiles.CSS_LEVEL_2`` (~ CSS 2.1) to prevent CSS 2.1 properties to be reported. Also other validation related output has been slightly changed.
**The way to change a defaultprofile may change again.**
- **API CHANGE**: ``cssutils.script.csscombine`` has **ONLY** keyword parameters now. Use ``csscombine(path=path[,...])`` for the old behaviour. New parameter ``url`` combines the sheet at URL now.
+ **FEATURE**: Added **experimental** profiles handling. You may add new profiles with new properties and their validation and set a defaultprofile used for validation. The current default profile is ``None`` so all predefined profiles are used. Currently 3 profiles are defined:
``~cssutils.profiles.Profiles.Profiles.CSS_LEVEL_2``
Properties defined by CSS2.1
``~cssutils.profiles.Profiles.Profiles.CSS_COLOR_LEVEL_3``
CSS 3 color properties
``~cssutils.profiles.Profiles.Profiles.CSS_BOX_LEVEL_3``
Currently overflow related properties only
See the docs and source of the cssutils.profiles module for details.
+ **FEATURE**: ``cssutils.util._readUrl()`` allows fetchers to pre-decode CSS content and return `unicode` instances, with or without a specified source encoding (integrated from patch of Issue Bitbucket #19).
+ **FEATURE**: URL fetch method checks if cssutils is run in GoogleAppEngine (GAE) (if ``import google.appengine`` is successful) and uses the GAE fetch methods instead of urllib2 in that case. So in result cssutils should run on GAE just as elsewhere.
+ **FEATURE**: Function ``cssutils.resolveImports(sheet)`` returns a new stylesheet with all rules in given sheet but with all @import rules being pulled into the top sheet.
+ FEATURE: CSSCombine script and helper function resolve nested imports now.
+ FEATURE: Script ``csscombine`` has new option ``-u URL, --url=URL URL to parse (path is ignored if URL given)`` now
+ BUGFIX: Other priority values than ``!important`` are parsed now. Nevertheless they log an ERROR or raise a SyntaxErr.
**TODO**: Preference setting. Profile?
- IMPROVEMENT: Added all known properties (by cssutils ;) to CSS2Properties.
+ DOCUMENTATION: Changed documentation generation from Epydoc and home grown HTML generation to Sphinx. Not all is perfectly markup'd yet but not too bad either...
0.9.6a0 081214
+ **FEATURE**: ``xml.dom.DOMException``\ s raised do now contain infos about the position where the exception occured. An exception might for example have been raised as::
raise xml.dom.SyntaxErr('the message', 10, 5)
(where 10 is the line and 5 the column of the offending text).
Therefor you may **not** simply use ``str(e)`` to get the exception message but you have to use ``msg, line, col = e.args[0], e.args[1], e.args[2]``. Additionally exceptions raised have attributes ``e.line`` and ``e.col``.
+ **FEATURE**: @page rule accepts named page selector now, e.g. ``@page intro`` or ``page main:left``.
+ **FEATURE**: Script ``cssparse`` has new option ``-u URL`` which parses the given URL.
+ **FEATURE**: Started implementation of CSS profiles...
- moved ``cssutils.css.cssproperties.cssvalues`` to ``cssutils.profiles.css2``
- added CSS Color Module Level 3 with properties ``color`` and ``opacity``. Not implemented are SVG color names.
- unknown properties raise a WARNING instead of INFO now
- refactored CSSValue and subclasses therefore there may be some minor changes in serializing or value reporting
+ ``getStringValue()`` returns a string without quotes or for URIs a value without surrounding ``url(...)`` now
+ FEATURE **experimental**: Added class ``CSSColor`` which is used for RGB, RGBA, HSL, HSLA and HEX color values of ``CSSValue`` respective ``CSSPrimitiveValue``.
+ FEATURE (strange): IE only CSS expressions *should* be parsed and serialized now an an ``Expression`` value. I have not tested this deeply and there may be problems but for some common cases this should work, e.g. for hacking maxwidth for IE you may define the following::
width: expression(document.body.clientWidth > 1000 ? "1000px": "100%")
Usage of CSS expressions is strongly discouraged as they do not validate AND may slow down the rendering and browser quite a lot!
+ BUGFIX/IMPROVEMENT: rewrite of CSSValue and related classes
- BUGFIX: Parsing of a CSSValue like ``red /**/`` (value, Space, comment) fixed.
- BUGFIX: Parsing values with ``+`` operator fixed.
- BUGFIX: ``CSSValueList.__str__`` added (missing last underscore rendered it useless)
- IMPROVEMENT: Serializing e.g. ``rgb(0,0,0)`` now defaults to ``rgb(0, 0, 0)``
- IMPROVEMENT: HEX values are minified if possible, e.g. ``Bitbucket #112233`` results in ``Bitbucket #123``
- IMPROVEMENT: Improved handling of zero lengths like ``-0, 0.0, .0mm`` etc all should come out simply as ``0`` now
- IMPROVEMENT: number values are normalized now, e.g. ``010.0px`` results in ``10px`` etc
- IMPROVEMENT: DIMENSIONs are normalized now, e.g. ``1pX`` results in ``1px``
- IMPROVEMENT: for CSSValueList values like e.g. ``margin: 0 1px`` there are no more false WARNINGs emitted
- IMPROVEMENT: Comments should be parsed much better now
- IMPROVEMENT: ``CSSValue.getFloattype(unitType=None)``, parameter is now optional in which case the current dimension is used.
- fixed lots of other minor bugs related to CSSValue
- **BUGFIX**: Fixed tokenizing/parsing of rather strange STRING and URI values like:
- Valid::
"somestring followed by escaped NL\
and continuing here."
and now results in::
"somestring followed by escaped NL and continuing here."
- ``url())`` => not allowed and must be written as ``url(")")``
- **BUGFIX**: Setting ``CSSPageRule.selectorText`` does actually work now.
- BUGFIX: Other priority values than ``!important`` are parsed now. Nevertheless they log an ERROR or raise a SyntaxErr.
- BUGFIX: Fixed Issue Bitbucket #14, added ``CSSStyleDeclaration().borderLeftWidth``. But prefer to use ``CSSStyleDeclaration()['border-left.width']``.
+ **API CLEANUP**:
- moved validating of a property from ``CSSValue`` to ``Property``
- removed ``CSSValue.valid`` as it does not make sense anymore
- removed private init parameter ``CSSValue_propertyName``
- private attribute ``CSSValue._value`` contains ``(value, type)`` now. Do not use as it may change again
- removed ``Property.normalname`` (DEPRECATED from 0.9.5 ), use ``Property.name`` instead
- removed long deprecated ``CSSStyleSheet.replaceUrls``, use ``cssutils.replaceUrls()`` instead
- ``cssutils.utils.Base`` and ``cssutils.utils.Base2`` have been changed and will be removed in favor of new ``cssutils.utils._NewBase``. These are all internal helper classes and should not be used in client code anyway but ye be warned...
+ IMPROVEMENT:
- minor changes due to deprecation in Py3k. cssutils is NOT Py3k compliant yet though and it will probably take a while until it will be...
v0.9.5
======
0.9.5.1 080811
+ **BUGFIX**: Fixed parsing of ``}a,b`` which resulted in TypeError until now.
+ **BUGFIX**: A rule with a selector using an undefined and therefor invalid namespace prefix is ignored now.
+ **BUGFIX**: Removed typo in MediaList which resulted in Exception when parsing medialist containing ``all`` and another media.
+ **BUGFIX**: Reading imported styles may have failed under certain conditions with an AttributeError.
+ FEATURE: Added ``cssutils.VERSION`` which is the current release version, in this case e.g. ``"0.9.5.1"``
+ IMPROVEMENT: Optimized imports and partly removed circular ones which are a bit tricky...
0.9.5 080730
+ **API CHANGE**: If a new medium is trying to be appended to a ``MediaList`` already set to ``all`` an ``xml.dom.InvalidModificationErr`` is raised. The exception to this handling is adding ``handheld`` which is a special case for Opera and kept for now. This special handling may be removed in the future. A ``WARNING`` is logged in any case.
- **BUGFIX**: Fixed reference error in @import rule preventing change of the used ``MediaList``.
- **BUGFIX**: Deeply nested ``CSSImportRule``\ s with different encodings should keep the encoding as defined (via HTTP, parendSheet, @charset etc) now. Therefor ``cssutils.util._readUrl`` does return ``(encoding, enctype, decodedCssText)`` now where ``enctype`` is a number from 0 to 5 indicating which encoding type was used: 0 for encoding override, 1 for HTTP encoding, 2 for BOM or @charset rule, (3 is unused currently), 4 for encoding of the parent sheet and 5 if encoding defaults to UTF-8 as no other information is available. (This may later be done as constants but this function should not be used from programs generally).
- **BUGFIX**: Replaced usage of ``WindowsError`` with ``OSError``. I (naively ;) thought ``WindowsError`` at least be present in environments other than Windows but it just results in a ``NameError``... The part of the API which triggered this Exception is an @import rule with an invalid or local (file) URI so should have happened quite rarely anyway.
+ IMPROVEMENT: Standalone scripts ``csscombine`` and ``csscapture`` are available for programmatic use in ``cssutils.script.csscombine`` and ``cssutils.script.CSSCapture`` res.
+ IMPROVEMENT: ``cssutils.script.csscombine`` and ``csscombine`` script do use the cssutils log now instead of just writing messages to ``sys.stderr``
+ IMPROVEMENT: Optimized and refactored tokenizer (CHARSET_SYM).
0.9.5rc2 080714
- **API CHANGE/BUGFIX (major)**:
Upto 0.9.5rc1 any sheet resulting from parsing via any ``parse*`` function or ``CSSParser(raiseExceptions=False)`` (which also was and is the default) resulted in the library simply logging any later exceptions and not raising them. Until now the global setting of ``cssutils.log.raiseExceptions=True`` (the default) was overwritten with the value of the CSSParser ``raiseExceptions`` setting which normally is ``False`` any time a ``cssutils.parse*`` function or ``CSSParser.parse*`` method was used. 0.9.5rc2 fixes this.
until 0.9.5rc1::
>>> # parsing does not raise errors
>>> s = cssutils.parseString('$') # empty but CSSStyleSheet object
>>> # using DOM methods does **not raise either** but should:
>>> s.cssText = '$' # just logs:
ERROR CSSStyleRule: No start { of style declaration found: u'$' [1:2: ]
from 0.9.5rc2::
>>> # parsing STILL does not raise errors
>>> s = cssutils.parseString('$') # empty but CSSStyleSheet object
>>> # using DOM methods **does raise now though**
>>> s.cssText = '$' # raises:
xml.dom.SyntaxErr: CSSStyleRule: No start { of style declaration found: u'$' [1:1: $]
To use the old but false behaviour add the following line at the start to your program::
>>> cssutils.log.raiseExceptions = False # normally True
**This should only be done in specific cases** as normal raising of exceptions in methods or functions with the CSS DOM is the expected behaviour. **This setting may also be removed in the future so use with care.**
- **BUGFIX**: Parsing of @rules like ``@mediaall ...`` does not result in ``@media all ...`` anymore (so not a ``CSSMediaRule``) but parses as ``@mediaall`` so a ``CSSUnknownRule``. The specification is not too clear here but it seems this is the way to go. To help finding typos like this probably is, for any found CSSUnknownRule (an unknown @rule) a WARNING is emitted now (but never an exception raised). These typos will most likely happen like e.g. ``@mediaall``, ``@importurl()``, ``@namespaceprefix"uri"`` or ``@pagename:left``.
- **BUGFIX**: Parsing of unicode escapes like ``\\abc`` followed by CR/LF this is now correctly combined as only a single whitespace character.
- **BUGFIX**: Adding a malformed ``stylesheets.MediaQuery`` to a ``stylesheets.MediaList`` does fail now, e.g.::
>>> # invalid malformed medialist (missing comma):
>>> sheet = cssutils.parseString('@media tv INVALID {a {top: 0;}}')
ERROR MediaQuery: Unexpected syntax. [1:11: INVALID]
ERROR MediaList: Invalid MediaQuery: tv INVALID
>>> # the actual rule exists but has default empty content, this may be
changed later as it can be seen as a bug itself
>>> sheet.cssRules[0]
cssutils.css.CSSMediaRule(mediaText=u'all')
>>> sheet.cssText
''
>>> # BUT: Unknown media type but as it is valid does parse:
>>> sheet = cssutils.parseString('@media tv, UNKNOWN {a {top: 0;}}')
WARNING MediaQuery: Unknown media type "UNKNOWN".
>>> sheet.cssRules[0]
cssutils.css.CSSMediaRule(mediaText=u'tv, UNKNOWN')
>>> sheet.cssText
'@media tv, UNKNOWN {\n a {\n top: 0\n }\n }'
- **BUGFIX**: References to ``MediaList`` in ``CSSImportRule`` and ``CSSMediaRule`` are kept now properly.
- BUGFIX: Deleting a ``MediaQuery`` item from a ``MediaList`` does use the libs logging/raising settings instead of always raising
- **IMPROVEMENT**: Parsing performance has been improved (by about 25%, tested with a basic CSS of about 50 lines, so may not be representative but this release definitely is faster ;). The following changes have been done which should not impact any actual stylesheet:
+ A ``BOM`` token is recognized at the start of a stylesheet only (may be swallowed by the CSS codec anyway).
+ A ``BOM`` token is not counted in the line/col reporting anymore so the following token has a line and col of 1 now
+ Tests for tokenizing with css2productions has been removed but this is never used in the library anyway
0.9.5rc1 080709
- **API CHANGE/FEATURE**: ``The cssutils.log`` may be partly used like a standard logging log. The following methods are available: ('setLevel', 'getEffectiveLevel', 'addHandler', 'removeHandler') as well as all "messaging" calls like 'error', 'warning' etc.
Therefor ``cssutils.log.setloglevel`` has been *DEPRECATED* and should be used via ``cssutils.log.setLevel``. The old method is still available though.
``cssutils.log.setlog`` has been renamed to ``cssutils.log.setLog`` but is still available but *DEPRECATED* too.
- **FEATURE**: All three decoders in the codec now have an additional ``force`` argument. If ``force`` is false, the encoding from the input will only by used if is is detected explicitely via BOM or @charset rule.
- **FEATURE**: ``cssparse`` script has new option ``-m --minify`` which results in the parsed CSS to be serialized minified
- **FEATURE**: ``CSSCapture`` and ``csscombine`` are now available not only as standalone scripts but also via ``cssutils.script.CSSCapture`` and ``cssutils.script.csscombine`` repectively so you can use them programmatically now.
- **BUGFIX**: A space after @rule keyword is added when serializing minified something like ``@media all{}``. Until now it was ``@mediaall{}`` which is recognized by Safari only but probably is not valid at all. Other @rules behave similar now too.
- **BUGFIX**: Properties of rules set via ``css.CSSStyleSheet.add`` or ``.insert`` were not set properly, e.g. ``parentStyleSheet`` or the stylesheet handling of new @import rules was buggy.
- BUGFIX: Encountering OSError during resolving @import does not throw an error anymore but the resulting CSSImportRule.styleSheet will have a value of ``None``. OSError will probably only happen when using ``parseFile``.
- **IMPROVEMENT/BUGFIX**: A style sheet with ``href == None`` (e.g. parsed with ``parseString()`` or build completely from scratch) uses ``os.getcwd()`` as its base href now to be able to resolve CSSImportRules.
- **IMPROVEMENT/BUGFIX**: Rewrote ``csscombine`` script which should be much more stable now and handles namespaces correctly. Nested imports are still not resolved yet but this may come in the next release.
- IMPROVEMENT/BUGFIX: Added catching of WindowsError to default fetcher (e.g. is a file URL references a file not present).
- **CHANGE/BUGFIX**: Redone ``csscapture`` script. A few minor method changes (parameter ``ua`` of ``capture`` has been replaced by init parameter) and lots of internal improvement has been done.
- CHANGE: ``CSSStyleSheet.add(rule)`` simply appends rules with no specific order in the sheet to the end of it. So e.g. COMMENTs, STYLE_RULEs, etc are appended while rules with a specific place are ordered-in as before (e.g. IMPORT_RULE or NAMESPACE_RULE). Until now rules of a specific type like COMMENTs were ordered together which does not really make sense. The ``csscombine`` script needs this functionality and the resulting combined sheets should be more readable and understandable now.
- CHANGE: Default URL fetcher emits an ERROR instead of a warning if finding a different mine-type than ``text/css``.
0.9.5b3 080605
- **API CHANGE**: ``parse()`` is *DEPRECATED*, use ``parseFile()`` instead. I know this should not happen in a release already in beta but better now than later and currently both ways are still possible.
- **FEATURE**: CSSStyleDeclatation objects may be used like dictionaries now. The value during setting a property may be a single value string or a tuple of ``(value, priority)``::
>>> style = css.CSSStyleDeclaration()
>>> style['color'] = 'red'
>>> style.getProperties()
[cssutils.css.Property(name='color', value=u'red', priority=u'')]
>>> del style['color']
>>> style['unknown'] = ('value', 'important')
INFO Property: No CSS2 Property: 'unknown'.
>>> style.getProperties()
[cssutils.css.Property(name='unknown', value=u'value', priority=u'impor
tant')]
>>> del style['never-set'] # does not raise KeyError but returns u'' like removeProperty()
>>>
- **FEATURE**: While reading an imported styleSheet all relevant encoding parameters (HTTP headers, BOM/@charset, etc) are used now as defined in http://www.w3.org/TR/CSS21/syndata.html#charset
Additionally a given parameter ``encoding`` for ``parseString``, ``parseFile`` and ``parseUrl`` functions/methods **overrides** any detected encoding of read sheet like HTTP information or @charset rules. Useful if e.g. HTTP information is not set properly. The given ``encoding`` is used for **all** imported sheets of the parsed one too! This is a cssutils only addition to the rules defined at http://www.w3.org/TR/CSS21/syndata.html#charset.
- **FEATURE**: A custom URL fetcher may be used during parsing via ``CSSParser.setFetcher(fetcher)`` (or as an init parameter). The so customized parser is reusable (as all parsers are). The fetcher is called when an ``@import`` rule is found and the referenced stylesheet is about to be retrieved.
The function gets a single parameter
``url``
the URL to read
and MUST return ``(encoding, content)`` where ``encoding`` normally is the HTTP charset given via a Content-Type header (which may simply omit the charset though) and ``content`` being the (byte) string content. The Mimetype of the fetched ``url`` should be ``text/css`` but this has to be checked by the fetcher itself (the default fetcher emits an ERROR (from 0.9.5 before a WARNING) if encountering a different mimetype).
The content is then decoded by cssutils using all encoding related data available.
Example::
def fetcher(url):
return 'ascii', '/*test*/'
parser = cssutils.CSSParser(fetcher=fetcher)
parser.parse...
To omit parsing of imported sheets just define a fetcher like ``lambda url: None`` (A single None is sufficient but returning ``(None, None)`` is more explicit).
You might also want to define an encoding for each imported sheet with a fetcher which returns a (normally HTTP content-type header) encoding depending on each URL.
- **FEATURE**: Added option ``-s --string`` to cssparse script which expects a CSS string to be parsed.
- **FEATURE/BUGFIX**: Parsing of CSSStyleDeclarations is improved. Invalid ``/color: red;color: green`` is now correctly parsed as ``color: green`` now. At the same time the until now parsed but invalid ``$color: red`` (an IE hack) is not parse anymore but correctly dismissed!
Unknown rules in CSSStyleDeclaration are parsed now. So e.g ``@x; color: red;`` which is syntactically valid is kept completely.
- **BUGFIX**: ``parseUrl`` does return ``None`` if an error occurs during reading the given URL. Until now an empty stylesheet was returned.
- **BUGFIX**: Fixed parsing of values like ``background: url(x.gif)0 0;`` (missing space but still valid).
- BUGFIX: Serializing CSSUnknownRules is slightly improved, blocks are correctly indentet now.
- **LICENSE**: cssutils is licensed under the **LGPL v3** now (before LGPL v2.1). This should not be a problem I guess but please be aware. So the former mix of LGPL 2.1 and 3 is resolved to a single LGPL 3 license for both cssutils and the included encutils.
- INTERNAL: Moved tests out of cssutils main package into a tests package parallel to cssutils.
0.9.5b2 080323
- **API CHANGE**: ``cssutils.parseURL`` has been renamed to ``parseUrl`` for consistency with ``getUrls`` or ``replaceUrls``. Parameter ``href`` (before called ``url``) is the first and mandatory parameter now.
+ **BUGFIX**: Fix the streamreader in the codec: Honor the encoding if one is passed to the constructor instead of trying to detect it from the stream.
+ **BUGFIX**: Reading referenced styleSheet in CSSImportRule did not succeed as no encoding information is passed along. Encoding of referenced sheets is always retrieved via HTTP or from imported sheet itself. Fixed lots of unchecked cases and simplified exception handling when reading a referenced sheet.
+ BUGFIX: Setting ``atkeyword`` of @rules checks if it is a valid keyword for the specific rule. E.g. an @import rule accepts ``@im\port`` but not ``@namespace``.
+ BUGFIX: Fixed setting ``name`` of CSSImportRule. Setting ``name`` other than with a string results in xml.dom.SyntaxErr raised now
+ BUGFIX: ``CSSStyleSheet.type`` with a fixed value of "text/css" and other readonly properties are really readonly now
- IMPROVEMENT: Added media information to ``__str__`` and ``__repr__`` or CSSStyleSheet.
- IMPROVEMENT: Added more information to ``__repr__`` of CSSImportRule.
- IMPROVEMENT: Added ``__repr__`` for ``cssutils.util._SimpleNamespaces`` which is used in a selector repr.
0.9.5b1 080319
- **API CHANGE**: ``cssutils.css.CSSSStyleSheet.replaceUrls(replacer)`` has been **DEPRECATED** but is available as an utility function so simply use ``cssutils.replaceUrls(sheet, replacer)`` instead. For the why see ``getUrls(sheet)`` below.
- **API CHANGE/FEATURE**: ``parseString`` has a new parameter ``encoding`` now which is used if a ``str`` is given for cssText. Otherwise it is ignored. (patch by doerwalter)
- API CHANGE/FEATURE: ``.parse() .parseString()`` and constructor of ``CSSStyleSheet`` have a new parameter ``title`` needed for the cascade (yet to be implemented ;).
Also the representation of ``CSSStyleSheet`` has been improved.
+ **FEATURE**: Referenced stylesheet in an @import rule is read and parsed now if possible. Therefor the ``href`` given during parsing (parameter ``href`` to the ``parse*`` functions is used. It is also properly set on imported rules. The ``name`` property of the @import rule is set as the imported sheets ``title`` property.
+ **FEATURE**: Added ``cssutils.getUrls(sheet)`` utility method to get all ``url(urlstring)`` values in ``CSSImportRules`` and ``CSSStyleDeclaration`` objects (properties). As this function and the above mentioned ``replaceUrls(sheet, replacer)`` are useful not only for a single sheet but (later) also for a stylesheet list they are not methods of CSSStyleSheet anymore (also because they are not part of the official DOM spec). (patch by doerwalter)
+ FEATURE: Added ``cssutils.parseURL(url, encoding=None, ...)``
+ BUGFIX: Fixes Issue Bitbucket #10, using full ``$LastChangedDate$`` in source files breaks code for some locales. Now only in a few files this svn:keywords replacement is used and only to a fixed length without the problematic part. In all other files ``$Id$`` is used which also includes simple but sufficient date information.
+ **BUGFIX/IMPROVEMENT**: Handling of trailing content, WS and comments in rules should be more consistent and properly handled now, added tests. Exception is ``CSSCharsetRule`` where no comments are allowed at all.
- TESTS: **Tests need ``minimock`` now!** Install with ``easy_install minimock``
+ DOCUMENTATION: Improved docs a bit.
- **LICENSE**: The included `encutils `__ has been updated to version 0.8.2 with a compatible LGPL license. `restserver.py `__ has been updated to version 2.1 which is in the public domain now (no Creative Commons license anymore). So only a single license (the LGPL) is used throughout cssutils now. If you have other licensing needs please let me know.
0.9.5a4 080222
- **API CHANGE**: ``.wellformed`` replaces ``.valid`` for most classes. This is more an internal value so should be used carefully anyway. Some classes still have both, notably ``Property`` and ``CSSValue``. Also removed ``Preferences.removeInvalid`` which was deprecated for some time now and made not really sense anyway.
- API CHANGE: Removed ``cssutils.ser.prefs.wellformedOnly`` which made no sense at all. It probably was not used in client code anyway. cssutils serializes wellformed (not necessarily valid) stylesheets only (hopefully ;).
- API CHANGE: Removed constructor parameter ``css.CSSImportRule(hreftype=...)`` which made no sense really. The type of href is still retained if ``css.CSSImportRule.cssText`` is set (e.g. for ``@import "a-str";`` it is "string" and for ``@import url(an-uri);`` it is "uri". How it is serialized is defined in the serializer anyway (``cssutils.ser.prefs.importHrefFormat`` "string", "uri" or None which then uses the one in each @import rule or defaults to "uri") so it made no sense to have it hear too. It still may be used but generally should not.
+ **FEATURE**: Defining a namespace with a prefix but an empty namespaceURI is not allowed in XML 1.0 (but in XML 1.1). It is allowed in CSS and therefor also in cssutils.
**ATTENTION**: CSS differs from XML 1.0 here!
+ **FEATURE**: Added property ``css.CSSImportRule.name`` and ``css.CSSMediaRule.name`` as decribed in http://www.w3.org/TR/css3-cascade/#cascading. It is parsed, serialized and available in this new property now. Property ``name`` is a constructor parameter now too.
+ **FEATURE**: ``css.UnknownRule`` is now parsed properly and checked for INVALID tokens or if {}, [] or () are not nested or paired properly. CSSUnknownRule is removed from CSSOM but in cssutils it is and will be used for @rules of programs using extensions, e.g. PrinceXML CSS. It is not very usable yet as no actual properties except ``atkeyword``, ``cssText`` and ``seq`` are present but at least it is syntactically checked properly and I hope serialized similar to other rules. This has been completely rewritten so may contain a few bugs so check your serialized sheets if you use non-standard @rules.
- **BUGFIX**: Improved escaping. Fixed cases where e.g. an URI is given as ``url("\"")``. Also escapes of delimiters in STRINGs is improved. This is used by ``CSSImportRule`` or ``CSSNamespaceRule`` among others. All STRING values are serialized with ``"..."`` (double quotes) now. This should not be a problem but please note that e.g. a ``CSSValue`` may be slightly different now (but be as valid as before).
- **BUGFIX**: Fixed serialization of namespaces in Selector objects. Actually all possible namespaced selectors should be preserved now:
``*``
any element or if a default namespace is given any element in that namespace
``a``
all "a" elements or if a default namespace is given "a" elements in that namespace
``|*``
any element in the no namespace (the *empty namespace*)
``|a``
"a" elements in the no namespace (the *empty namespace*)
``*|*``
any element in any namespace including the no namespace
``*|a``
"a" elements in any namespace including the no namespace
``p|*``
any element in the namespace defined for prefix p
``p|a``
"a" elements in the namespace defined for prefix p
- **BUGFIX**: Default namespace is no longer used by attribute selectors.
+ CHANGE: ``CSSNamespaceRule`` implements http://dev.w3.org/csswg/css3-namespace/ now. Giving the namespaceURI as an URI token (``url("uri")``) is no longer deprecated so no warning is emitted any longer.
- IMPROVEMENT: Started refactoring rules to have ``wellformed`` property and serializing included comments better.
- IMPROVEMENT: Added simple testtool for functional tests in /examples plus lots of smaller bugfixes, improvements and refactorings
0.9.5a3 080203
- **API CHANGE: Refactored and fixed namespace handling**
Aim was to prevent building invalid style sheets. therefor namespaces must be checked e.g. when adding a new ``Selector`` etc. This probably is not fixed for all cases but much better now than before.
- added ``CSSStyleSheet.namespaces`` which is a mapping of ``prefix: namespaceURI`` and mirrors all namespaces as defined in @namespace rules. New Namespaces may also be set here as well as prefixes changed.
- if more than one ``CSSNamespaceRule`` with the same ``namespaceURI`` is set only the last one will be kept. The ``prefix`` of that rule is used.
- ``CSSNamespaceRule.namespaceURI`` is readonly now, it can only be set in the constructor (needed to prevent an invalid sheet when changing this uri)
- Namespaces used in a Selector or SelectorList or even a CSSStyleRule while these are not attached to a CSSStyleSheet (which would contain the necessary CSSNamespaceRules) are kept internally. All these classes accept for parameter ``selectorText`` (or ``cssText`` for CSSStyleRule) a tuple of ``(string-to-parse, dict-of-namespaces)`` now while not attached to a style sheet. If attached ``dict-of-namespaces`` is ignored as the namespaces of the relevant style sheet are used. If you need to set e.g. a new selector within a yet undefined namespace, set the namespace in the style sheet first simply by setting e.g. ``sheet[prefix] = namespaceURI``
- removed ``CSSStyleSheet.prefixes``
- removed ``Selector.prefixes``
- **API CHANGE**: ``parentRule`` and ``parentStyleSheet`` of all CSS rules are now readonly to prevent building illegal style sheets.
- **API CHANGE**: Changed order of constructor parameters for ``CSSStyleDeclaration``. Named parameters SHOULD be used anyway but be careful if you used ordered ones here!
* **FEATURE**: ``CSSStyleSheet`` and ``CSSMediaRule`` are iterable now. Both iterate on their ``cssRules``. (Internally generators are used.)
* **FEATURE**: added convinience method ``CSSStyleSheet.add(rule)`` which behaves exactly like ``.insertRule(rule, inOrder=True)``. So rules are added to the approprite position in a style sheet. E.g a namespace rule if put after any @import but before other rules like style or font-face rules.
* **FEATURE**: added parameter ``inOrder=False`` to method ``CSSStyleSheet.insertRule`` which inserts a given rule to its proper hierarchy. Parameter ``index`` is ignored in this case but the resulting index is properly returned.
* FEATURE: added convinience method ``CSSMediaRule.add(rule)`` which behaves exactly like ``.insertRule(rule)`` (there is no parameter "inOrder" here as no invalid order may be build in CSS 2.1 or 3).
* FEATURE: Added ``Selector.parentList`` which contains a reference to the containing ``SelectorList``. Needed (at least) to process namespaces properly.
+ **BUGFIX**: ``CSSMediaRule.insertRule`` setting with a rule string fixed
+ **BUGFIX**: ``*.parentStyleSheet`` and ``*.parentRule`` where * is any CSSRule is properly set now
+ **BUGFIX**: ``CSSStyleDeclatation.getPropertyPriority(p)`` returns ``important`` (without the ``"!"``!) or the empty string now (see http://dev.w3.org/csswg/cssom/#the-cssstyledeclaration). Same goes for ``Property.priority`` which is not in CSSOM but cssutils only.
(``Property._normalpriority`` has been removed, the normalized value that was available here is now in ``Property.priority``. The literal priority value is available in ``Property.literalproperty`` now (analog to ``Property.literalname``). All these values probably should not be used by client code anyway but may be helpful when using CSS hacks.)
+ BUGFIX: Changed serialization of combinators in ``Selector`` according to http://dev.w3.org/csswg/cssom/#selectors, e.g. ``a>b+c~d e`` serializes as ``a > b + c ~ d e`` now (single spaces around +, > and ~). A new serializer preference ``selectorCombinatorSpacer = u' '`` has been added to overwrite this behaviour (which is set to ``u''`` when using the CSS minifier settings)
+ BUGFIX: Some minor fixes including some reference improvements
- IMPROVEMENT: csscombine is available via ``from cssutils.scripts import csscombine`` now (not directly in cssutils though!). Some optimizations and comments added.
0.9.5a2 080115
+ **BUGFIX**: Serializing a ``CSSStyleDeclaration`` did not include the effective properties but the last property if set multiple times in a single declaration and preferences ``keepAllProperties == False``.
+ BUGFIX: Fixed parsing of last remaining example in CSS spec ``color:red; color{;color:maroon}; color:green`` which now correctly parses ``color: green``
+ BUGFIX: ``CSSStyleDeclaration.__contains__(property)`` uses not the literal but the normalized name (``Property.name``) to check if a property is set in this declaration now
+ BUGFIX: ``CSSStyleDeclaration.getProperties(all=True)`` failed if comments were present
0.9.5a1 080113
+ **API CHANGE**: ``Property.name`` is now the same as ``Property.normalname`` which is DEPRECATED now. To access the literal name (the value which was available in ``name`` until now) use ``Property.literalname``. For most cases where a property name is used the new behaviour makes more sense, therefor the change.
**Do not use ``normalname`` anymore, it will probably be removed for release 1.0.**
NEW since 0.9.5::
p = Property(ur'c\olor', 'red')
p.name == ur'color'
p.literalname == ur'c\olor'
# DEPRECATED: p.normalname == ur'color'
OLD until 0.9.5::
p = Property(ur'c\olor', 'red')
p.name == ur'c\olor'
p.normalname == ur'color'
+ **API CHANGE**: iterating over ``css.CSSStyleDeclaration`` yields now *effective* properties only and not *all* properties set in the declaration. E.g. from ``color: red; c\olor: green`` only one Property is returned which has the value ``green``. To retrieve *all* properties use ``CSSStyleDeclaration.getProperties(all=True)``. Reason for this change is that for most cases the new default makes more sense.
- **FEATURE**: ``css.CSSStyleDelcaration`` supports ``in`` now. Expected is a Property or a name of a property which is checked if already in the style declaration
- **FEATURE**: ``css.Selector`` has a **readonly** property ``specificity`` now which is calculated as described at http://www.w3.org/TR/css3-selectors/#specificity
**ATTENTION**: changing the Selector by changing its property ``seq`` does **not** update the specificity! ``Selector.seq.append`` has been made private therefor and writing to ``seq`` **not** be used at all!
- **FEATURE**: Added ``css.CSSStyleDeclaration.getProperty(name, normalize=True)`` which returns the effective Property object for ``name``.
- FEATURE: Implemented http://csswg.inkedblade.net/spec/css2.1#issue-23, URI may be ``URL(...)`` or ``u\r\6c(...)`` now
+ **BUGFIX**: ``CSSStyleDeclaration.removeProperty(name, normalize=True)`` removes all properties with given ``name`` now and returns the effective value. If ``normalize==False`` removes only properties with ``name=Property.literalname`` and also only returns the effective value of the unnormalized name.
+ **BUGFIX**: Priority of Properties is acknowledged by all methods of ``css.CSSStylesDeclaration`` now.
+ **BUGFIX**: Fixed serializing of strings like "\\a", "\\22" and '\\27' in @import urls, selectors and values. **func("string") is not yet fixed!!!**
- CHANGE: ``CSSValueList`` does not emit warnings for shorthand properties anymore. Nevertheless these may be invalid!
- IMPROVEMENT: ``CSSStyleDeclaration`` and some minor other parts refactored
0.9.4b1 071229
- **FEATURE**: Added ``csscombine`` script which currently resolves @import rules into the input sheet. No nested @imports are resolved yet and @namespace rules do not work yet though!
- FEATURE: ``css.CSSStyleSheet.insertRule(rule, index)`` accepts now a ``css.CSSRuleList`` in addition to a ``css.CSSRule`` object or a CSS string. Useful if you like a combine a complete sheet into an existing one.
+ **BUGFIX**: Serializing escape sequences add a single SPACE after each escape. This was not present until now so a sequence like "\\74 a" did come out as "\\000074a" which was not as intended. Also as a SPACE is inserted in any case all escapes are not padded to 6 digits anymore but are only as long as needed.
+ **BUGFIX**: Handling of illegal selectors is now same as the W3C CSS validator (and according the selector spec - I hope ;). Illegal selectors result the complete rule being dropped. Fixed are the following (edge) cases:
``a/**/b``
Meant was probably a space between a and b (plus maybe the comment) but it MUST be inserted. IE and Safari nevertheless seem to parse this rule as ``a b`` so as if a space would be present. cssutils now parses this selector as intented by the spec as ``ab``.
``a*b``
Again spaces around the UNIVERSAL ``*`` were probably meant by the author. IE and Safari seem to parse this **invalid** selector as ``a b``. cssutils ignores this rule completely!
+ BUGFIX: ``css.CSSRuleList`` is still a Python list but setting methods like ``__init__``, ``append``, ``extend`` or ``__setslice__`` are added later on instances of this class if so desired. E.g. CSSStyleSheet adds ``append`` which is not available in a simple instance of this class! This has been changed as no validation is possible in CSSRuleList itself.
- CHANGE: Unknown media type in a MediaQuery (e.g. ``@media tv, radio``) does emit WARNING instead of ERROR now.
+ IMPROVEMENT: Added better ``str`` and ``repr`` to cssutils.serializer.Preferences
+ IMPROVEMENT: Added position information to some error reportings (Property, CSSMediaRule
0.9.4a4 071202
- **FEATURE**: Implemented ``css.CSSFontFaceRule``.
- **FEATURE**: Added ``css.CSSStyleSheet.encoding`` which reflects the encoding of an explicit @charset rule. Setting the property to ``None`` removes an @charset rule if present and sets the encoding to the default value 'utf-8'. Setting a value of ``utf-8`` sets the encoding to the default value too but the @charset rule is explicitly added.
Effectively this removes the need to use ``css.CSSCharsetRule`` directly as using this new property is easier and simpler.
(A suggestion in the `CSSOM `_ but not yet resolved. IMHO it does make sense so it is present in cssutils. ``css.CSSCharsetRule`` remains though if you really *want* to use it).
+ **BUGFIX/IMPROVEMENT**: ``css.SelectorList`` and ``stylesheets.MediaList`` have (Python) list like behaviour partly but are *directly not lists anymore* (which did not work properly anyway...). The following list like possibilities are implemented for now:
- ``item in x`` => bool
- ``len(x)`` => integer
- get, ``del`` and set ``x[i]``
- ``for item in x``
- ``x.append(item)``
The DOM additional methods and properties like ``length`` or ``item()`` are still present (and also will be in the future) but the standard Python idioms are probably easier to use.
``stylesheets.StyleSheetList`` and ``css.CSSRuleList`` are the only direct lists for now. This may change in the future so it is safer to also use the above possibilities only for now.
+ BUGFIX: Fixed handling of "\\ " (an escaped space) in selectors and values.
+ BUGFIX: ``!important`` is normalized (lowercase) now
- IMPROVEMENT: Some error messages have been changed slightly, mostly values are given with their Python representation and not the actual values.
- IMPROVEMENT: The setup process of cssutils has been adapted to suggestions at http://jimmyg.org/2007/11/08/creating-a-python-package-using-eggs-and-subversion/
+ DOCS: Slight overhaul of docs.
0.9.4a3 071106
+ CSSCapture:
+ **FEATURE**: Added option ``-m, --minified`` to CSSCapture which saves the retrieved CSS files with the cssutils serializer setting ``Preferences.useMinified()``.
- **BUGFIX**: option '-p' of csscapture is removed as it was not used anyway. A new option ``-r, --saveraw`` has been added which defaults to ``False``. If given saves raw css otherwise cssutils' parsed files.
- **BUGFIX**: CSSCapture now uses the ``cssutils.parseString`` method so invalid sheets should be saved too. Until now in case of an error the sheet was not saved at all.
- **BUGFIX/FEATURE**: Handling of unicode escapes should now work propertly.
The tokenizer resolves any unicodes escape sequences now so cssutils internally simple unicode strings are used.
The serializer should serialize a CSSStyleSheet properly escaped according to the relevant encoding defined in an @charset rule or defaulting to UTF-8. Characters not allowed in the current encoding are escaped the CSS way with a backslash followed by a uppercase 6 digit hex code point (**always 6 digits** to make it easier not to have to check if no hexdigit char is following).
This *FEATURE* was not present in any older version of cssutils.
- **BUGFIX**: Names (of properties or values) which are normalized should be properly normalized now so simple escapes like ``c\olor`` but also unicode escapes like ``\43olor`` should result in the property name ``color`` now
- **BUGFIX**: Selector did fail to parse negation ``:not(`` correctly
- **BUGFIX**: CSSValueList treated a value like ``-1px`` as 2 entries, now they are correctly 1.
- **BUGFIX**: Validation of values for ``background-position`` was wrong.
- **BUGFIX**: ``CSSPrimitiveValue.primitiveValue`` was not recognized properly if e.g. a CSS_PX was given as '1PX' instead of '1px'.
- **BUGFIX/CHANGE**: Reporting of line numbers should have improved as ``\n`` is now used instead of ``os.linesep``.
+ **CHANGE**: Invalid Properties like ``$top`` which some UAs like Internet Explorer still are use are preserved. This makes the containing Property and CSSStyleDeclaration invalid (but still *wellformed* although they technically are not) so if the serializer is set to only output valid stuff they get stripped anyway.
**This may change and also simply may be put in a cssutils wide "compatibility mode" feature.**
+ **CHANGE**: If a CSSValue cannot be validated (no property context is set) the message describing this is set to DEBUG level now (was INFO).
+ IMPROVEMENT: "setup.py" catches exception if setuptools is not installed and emits message
- DOCS: Added more documentation and also a standalone HTML documentation which is generated from the SVN RST docs.
0.9.4a2 071027
- **FEATURE**: added ``Preferences.useMinified()`` which sets preferences that a stylesheet will be serialized as compact as possible. Added ``Preferences.useDefaults()`` which resets the serializer preferences. There a few new preferences have been added as well (see the documentation for details as most are hardly useful for normal usage of the library)
+ **BUGFIX**: Fixed parsing of ``font`` value which uses "font-size/line-height" syntax.
- CHANGE: ``Preferences.keepAllProperties`` defaults to ``True`` now (hardly used but safer if different values have been set which are used by different UAs for example.)
0.9.4a1 071021 (new parser [again])
- **FEATURE**: Added a new module ``cssutils.codec`` that registers a codec that can be used for encoding and decoding CSS. (http://www.w3.org/TR/2006/WD-CSS21-20060411/syndata.html#q23)
- **FEATURE**: Added implementation of ``stylesheets.MediaQuery`` which are part of stylesheets.MediaList. See the complete spec at http://www.w3.org/TR/css3-mediaqueries/ for details.
Not complete yet: Properties of MediaQueries are not validated for now and maybe some details are missing
- FEATURE: Implemented ``cssutils.DOMImplementationCSS``. This way it is possible to create a new StyleSheet by calling ``DOMImplementationCSS.createCSSStyleSheet(title, media)``. For most cases it is probably easier to make a new StyleSheet by getting an instance of ``cssutils.css.CSSStyleSheet`` though.
- FEATURE: cssutils is registered to ``xml.dom.DOMImplementation`` claiming to implement CSS 1.0, CSS 2.0, StyleSheets 1.0 and StyleSheets 2.0. This is probably not absolutely correct as cssutils currently is not a fully compliant implementation but I guess this is used very rarely anyway.
+ **API CHANGE**: ``CSSNamespacerule.uri`` is renamed to ``CSSNamespaceRule.namespaceURI`` which is defined is CSSOM. ``uri`` is deprecated and still available but the constructor parameter is named ``namespaceURI`` in any case now.
+ **API CHANGE**: As ``stylesheets.MediaQuery`` is implemented now all classes using an instance of ``stylesheets.MediaList`` are presented slightly different. Until now a simple list of string was given, now the list contains MediaQuery objects.
+ **API CHANGE**: ``_Property`` has been renamed to ``css.Property`` and is used in context of ``CSSStyleDeclaration`` and ``MediaQuery``. Attribute ``Property.value`` has been *de-deprecated* and may be used normally now (again). The Property constructor has only optional parameters now.
+ **API CHANGE**: Removed experimental class ``SameNamePropertyList`` which was used in ``CSSStyleDeclaration`` and also method ``CSSStyleDeclaration.getSameNamePropertyList``. A new method ``CSSStyleDeclaration.getProperties()`` has been added which is simpler and more useful
+ **API CHANGE**: renamed attribute ``namespaces`` of CSSStyleSheet and Selector to ``prefixes`` as they really are the prefixes of declared namespaces
- API CHANGE (internal): renamed ``Serializer.do_css_Property`` to ``Serializer.do_Property`` as it is ``Property`` is not in the official DOM, may not stay in package ``css`` and is used by MediaQuery too
- API CHANGE (internal): renamed ``Serializer.do_CSSvalue`` to ``Serializer.do_CSSValue``
+ BUGFIX: Tantek hack (using ``voice-family``) should work now as SameNamePropertyList is removed and properties are kept in order
+ BUGFIX: Token CHARSET_SYM is now as defined in the CSS 2.1 Errata as literal "@charset " including the ending space.
- **CHANGE**: A completely new tokenizer and mostly also the parser have been reimplemented in this release. Generally it should be much more robust and more compliant now. It will have new errors and also some slight details in parsing are changed.
+ **DOCS**: Added some docs in reStructuredText format including a basic server to view it as HTML. The HTML may be published as well.
0.9.3a1 - 070905
- FEATURE: Implemented css.CSSValue, css.CSSPrimitiveValue and css.CSSValueList.
Not yet implemented are:
- css.CSSPrimitiveValue.getCounterValue and css. Counter
- css.CSSPrimitiveValue.getRGBColorValue and css.RGBColor
- css.CSSPrimitiveValue.getRectValue and css.Rect
+ FEATURE: css.CSSValueList is iterable so may be used in a for loop
+ FEATURE: CSSValue has property ``cssValueTypeString`` which is the name of the relevant ``cssValueType``, e.g. "CSS_PRIMITIVE_TYPE". Mainly useful for debugging.
+ FEATURE: CSSPrimitiveValue has property ``primitiveTypeString`` which is the name of the relevant ``primitiveType``, e.g. "CSS_PX". Mainly useful for debugging.
+ CSSValue has an init Parameter ``_propertyname`` to set a context property for validation. If none is set the value is always invalid. **THIS MAY CHANGE!**
- FEATURE (**experimental**): CSSStyleDeclaration is iterable now. The iterator returns *all* properties set in this style as objects with properties ``name``, ``cssValue`` and ``priority``. Calling CSSStyleDeclaration.item(index) on the other hand simply returns a property name and also only the normalized name (once). Example::
sheet = cssutils.parseString('a { color: red; c\olor: blue; left: 0 !important }')
for rule in sheet.cssRules:
style = rule.style
for property in style:
name = property.name
cssValue = property.cssValue
priority = property.priority
print name, '=', cssValue.cssText, priority
# prints:
# color = red
# c\olor = blue
# left = 0 !important
for i in range(0, style.length):
name = style.item(i)
cssValue = style.getPropertyCSSValue(name)
priority = style.getPropertyPriority(name)
print name, '=', cssValue.cssText , priority
# prints:
# color = blue
# left = 0 !important
**ATTENTION**: This has been changed in 0.9.5, see details there!
- FEATURE (**experimental**): added ``CSSStyleSheet.replaceUrls(replacer)`` which may be used to adjust all "url()" values in a style sheet (currently in CSSStyleDeclaration and CSSImportRules).
- FEATURE: added ``CSSStyleDeclaration.getCssText(separator=None)`` which returns serialized property cssText, each property separated by given ``separator`` which may e.g. be u'' to be able to use cssText directly in an HTML style attribute. ";" is always part of each property (except the last one) and can **not** be set with separator!
- FEATURE: ``href`` and ``media`` arguments can now be passed to ``parse()`` and ``parseString()`` functions and methods. This sets the appropriate attributes on the generated stylesheet objects.
- FEATURE: CSSMediaRule has an init parameter ``mediaText`` synchronous to CSSImportRule now
- FEATURE: The ``MediaList`` constructor can now be passed a list of media types.
- FEATURE: ``CSSRule`` and subclasses have a property ``typeString`` which is the name of the relevant ``type``, e.g. ``STYLE_RULE``. Mainly useful for debugging.
- FEATURE: ``cssutils.serialize.Preferences`` has a new option ``lineSeparator`` that is used as linefeed character(s). May also be set to ``u''`` for ``CSSStyleDeclareation.cssText'`` to be directly usable in e.g. HTML style attributes
+ API CHANGE (internal): renamed serializers method ``do_stylesheet`` to ``do_CSSStyleSheet``
- BUGFIX (Bitbucket #9): Parsing of empty ``url()`` values has been fixed
- BUGFIX: Handling of linenumbers in the serializer has been fixed.
- BUGFIX (minor): removed debug output in CSSStyleDeclaration
+ CHANGE (experimental!): CSSStyleDeclaration.getPropertyCSSValue() for shorthand properties like e.g. ``background`` should return None. cssutils returns a CSSValueList in these cases now. Use with care as this may change later
+ CHANGE: CSSValue default cssText is now ``u""`` and not ``u"inherit"`` anymore
+ CHANGE: ``css.CSSStyleDeclaration.cssText`` indents its property not anymore.
+ CHANGE: ``cssutils.serialize.CSSSerializer`` has been refactored internally to support the lineSeparator option.
+ CHANGE: The Selector and SameNamePropertyList (which might be renamed as it is experimental) class are now available from cssutils.css too.
**UPDATE: SameNamePropertyList removed in 0.9.4**
+ CHANGE: Tokenizer strips HTML comment tokens CDO and CDC from tokenlist now.
+ CHANGE: Added __repr__ and __str__ methods to most classes. __str__ reports e.g. ````, __repr__ e.g. ``cssutils.css.CSSImportRule(href=None, mediaText=u'all')`` which is a valid contructor for the object in most cases (which might not be complete for all init parameter for all classes like in this case though). The following details are included:
css
- CSSStyleSheet shows the title and href
- CSSCharsetRule shows the encoding
- CSSCharsetRule shows the cssText (not in __str__ though)
- CSSImportRule shows the href and the MediaList mediaText
- CSSMediaRule shows the MediaList mediaText
- CSSNameSpaceRule shows the prefix and uri
- CSSPageRule shows the selectorText
- CSSStyleRule shows the selectorText
- CSSUnknownRule shows nothing special
- CSSStyleDeclaration shows the number of properties set for __str__ but an empty CSSStyleDeclaration constructor for __repr__ as showing cssText might be way too much
- SameNamePropertyList shows the name
- CSSValue, CSSPrimitiveValue show the actual value for __repr__, some details for __str__
- CSSValueList shows an __repr__ which is **not** possible to ``eval()`` and some details for __str__
- _Property shows infos but should be used directly for now anyway!
- Selector the selectorText
stylesheets
- MediaList shows the mediaText
0.9.2b3 070804
- FEATURE: Script ``cssparse`` handles more than one file at a time now (patch from Issue Bitbucket #6 by Walter Doerwald)
- BUGFIX: Fixed Issue Bitbucket #7: typo gave AssertionError for selectors like ``tr:nth-child(odd) td{}``
- BUGFIX: Fixed Issue Bitbucket #5: false warning for certain values for ``background-position`` removed
- BUGFIX: Report of line/col for any node was not correct if a node contained line breaks itself
- Quite a few internal optimizations (thanks to Walter Doerwald)
- Added tests for issues Bitbucket #3 and Bitbucket #4 to tokenizer too
0.9.2b2 070728
- BUGFIX: Fixed Issue Bitbucket #4, tokenizing of color values like ``Bitbucket #00a`` was buggy (mixture of numbers and characters). Also warnings of invalid property values should be more reliable now (regexes in ``css.cssproperties`` changed).
0.9.2b1 070726
- BUGFIX: Fixed Issue Bitbucket #3, WS was not handled properly if added to token list by tokenizer
0.9.2a5 070624
- BUGFIX: Unexpected end of style sheet now handled according to spec for most cases, e.g. incomplete CSSStyleRule, CSSMediaRule, CSSImportRule, CSSNamespaceRule, CSSPageRule.
0.9.2a4 070620
- BUGFIX (major): no changes to the library, but fixed setup of source dist
0.9.2a3 071018
- no changes to the library, just optimized setuptools dist
0.9.2a2 070617
- API CHANGE: removed cssutils.util.normalize function, use static (but private!) method cssutils.util.Base._normalize if absolutely needed which may be change too though
- API CHANGE (minor): removed ``getFormatted`` and ```pprint`` from various classes which were both DEPRECATED for some time anyway
- API CHANGE (minor): _Property.value is DEPRECATED, use _Property.cssValue.cssText instead, _Property is defined as private anyway so should not be used directly
- API CHANGE (minor): removed ``Tokenizer.tokensupto`` which was used internally only
- CHANGE: Numbers and Dimensions starting with "." like ".1em" in the original stylesheet will be output as "0.1em" with a proceding 0 now.
- CHANGE: Report of parsing errors have a slightly different syntax now.
- FEATURE: New ``Preferences.omitLastSemicolon`` option. If ``True`` omits ; after last property of CSSStyleDeclaration
- BUGFIX: The css validator definition for "num" was wrong, so values like ``-5.5em`` would issue a warning but should be correct
- BUGFIX: Dimension were not parsed correcly so 1em5 was parsed a "1em" + 5 which should really be one "1em5" were "em5" is an unknown dimension. This had probably no effect on current stylesheets but was a tokenizing error
- BUGFIX: Parsing of nested blocks like {}, [] or () is improved
- BUGFIX: Comment were not parsed correctly, now ``/*\*/`` is a valid comment
- BUGFIX: ``css.Selector`` had a warning which called "warning" which in fact is named "warn". Some other error messages gave token list instead of a more useful string in case of an error, that is fixed as well (CSSComment and CSSValue).
- IMPROVEMENT: Line number are still not given for all errors reported but for at least some more now
- IMPROVEMENT: Performance of the tokenizer has been improved, it is now about 20% faster (testing the unittests) which may not hold for all usages but is not too bad as well ;)
0.9.2a1 070610
- FEATURE: Partly Implemented css.CSS2Properties so you can now use::
>>> sheet = cssutils.parseString('a { font-style: italic; }')
>>> style = sheet.cssRules[0].style
>>> style.fontStyle = 'normal'
>>> print style.fontStyle
normal
Each property can be retrieved from CSSStyleDeclaration object with its name as
an object property. Names with "-" in it like ``font-style`` need to be called by
the respective DOM name ``fontStyle``.
Setting a property value works the same way and even ``del`` which effectively removes a property from a CSSStyleDeclaration works. For details see CSSStyleDeclaration.
Not implemented are the finer details, see the module documentation of
cssutils.css.cssproperties.
- BUGFIX: CSSStyleDeclaration.getPropertyCSSValue returns None for all shorthand properties
- refactored some parts and added more tests
0.9.1b3 070114
- **CHANGE** for Serializer preference options:
new name
+ ``defaultAtKeyword`` instead of ``normalkeyword``
+ ``defaultPropertyName`` instead of ``normalpropertyname``
camelcase now:
+ ``keepComments`` instead of ``keepComments``
+ ``lineNumbers`` instead of ``linenumbers``
replaced (see below)
+ ``keepAllProperties`` instead of ``keepsimilarnamedproperties``
- FEATURE: ``Serializer.prefs.keepAllProperties`` replaces `` ``keepsimilarnamedproperties``:
if ``True`` all properties given in the parsed CSS are kept.
This may be useful for cases like::
background: url(1.gif) fixed;
background: url(2.gif) scroll;
Certain UAs may not know fixed and will therefor ignore property 1 but
an application might simply like to prettyprint the stylesheet without
loosing any information.
Defaults to ``False``.
See examples/serialize.py for an usage example.
- FEATURE(experimental, might change!):
``CSSStyleDeclaration.getSameNamePropertyList(name)``
Experimental method to retrieve a SameNamePropertyList object which
holds all Properties with the given ``name``. The object has an
attribute ``name`` and a list of Property objects each with an actual name,
value and priority.
**UPDATE: SameNamePropertyList removed in 0.9.4**
``CSSStyleDeclaration.setProperty`` has a new positional parameter
``overwrite`` which defines if the property which is set overwrites any former
value (or values, see ``getSameNamePropertyList``) (default behaviour) or the
given value is appended to any former value (overwrite=False).
Useful for cases where a property should have different values for different UAs.
Example 1: CSS hacks::
width: 100px; /* wrong box model value for IE5-5.5 */
padding: 5px;
w\idth: 90px; /* correct box model value for later browsers */
Example 2: UA capabilities::
background: url(2.gif) scroll; /* Fallback for UA which do not understand fixed */
background: url(1.gif) fixed; /* UA which do know fixed */
- FEATURE: Reimplemented csscapture, which uses the new serializer preference ``keepAllProperties``
- BUGFIX(major!): Serializer outputs actual property depending on Property priority out now
see ``examples/serialize.py``
- BUGFIX(minor): Parameter ``name`` for `CSSStyleDeclaration.XXX(name)``
is normalized now, so ``color``, ``c\olor`` and ``COLOR`` are all equivalent
0.9.1b2 070111
- FEATURE: added ``Serializer.prefs.keepsimilarnamedproperties``:
if ``True`` all properties with the same normalname but different
actual names are kept, e.g. color, c\olor, co\lor.
This is mainly useful to keep a stylesheet complete which uses
xbrowser hacks as above.
**UPDATE IN 0.9.1b3!**
- BUGFIX (minor): ``Serializer.prefs.normalpropertyname`` did not work properly if a property was set 2 times in the same declaration, e.g. ``color: red;c\olor: green`` setting the pref to ``False`` results in ``c\olor: green`` now.
- BUGFIX (minor): Serializing of CSSStyleDeclaration did not work well when CSSComments were mixed with Properties.
0.9.1b1
- FUTURE CHANGE: ``readonly`` will be removed from most rules. It is not used anyway, may be readded in a future release
- CHANGE: order of constructor parameters changed in ``CSSImportRule``. Should be no problem as positional parameters are discouraged anyway
- CHANGE: cssutils needs Python 2.4 from the release on as it uses the buildin ``set``
- CHANGE: removed ``CSSMediaRule.addRule`` which was deprecated anyway
- FEATURE: implemented @page CSSRule including testcases
- FEATURE: implemented @namespace CSSRule according to http://www.w3.org/TR/2006/WD-css3-namespace-20060828/ with the following changes
* the url() syntax is not implemented as it may (?) be deprecated anyway
* added namespace parsing to ``Selector``, see http://www.w3.org/TR/css3-selectors/
* CSSStyleSheet checks if all namespaces in CSSStyleRules have been declared with CSSNamespaceRules. If not the rule's ``valid`` property is set to ``False`` and the serializer omits it (you may change ``Preferences.removeInvalid`` to change this behaviour).
* CSSStyleSheet and Selector object have a new propery ``namespaces`` which currently contain declared and used namespace prefixes (!), this may change in the future so use with care if at all.
- FEATURE: implemented ``CSSRule.parentStyleSheet`` for all rules
- FEATURE: implemented ``CSSRule.parentRule`` for relevant rules (all allowed in @media)
- BUGFIX: Set ``parentStyleSheet`` and ``parentRule`` as instance vars in ``css.CSSRule`` instead as class vars
- BUGFIX: CSSComment raised exception if setting cssText with empty string - fixed
- DOCS: generated docs with epydoc which are then included in src dist. Source documentation is cleaned up a bit.
- INTERNAL: Refactored some unittests
- INTERNAL: implementation based on `DOM Level 2 Style Recommendation `_ as opposed to the `Proposed Recommendation `_ now. As there are no main changes I could find this does not make any difference...
0.9.1a1
- CHANGE, renamed ``Serializer.prefs.srcatkeyword`` to ``Serializer.prefs.normalkeyword``
which work just the other way round but work as ``Serializer.prefs.normalpropertyname``
- BUGFIX in css.Selector and added support regarding handling of pseudoclasses (``:x`` or ``:x()``) and pseudoelements ``::x``
- BUGFIX and refactoring in tokenizer, mostly regarding escape sequences
* combination of \ and NEWLINE in a string is removed according to spec now
- added ``Serializer.prefs.normalpropertyname``, if True, property names are normalized if known (``color``), else literal form from CSS src is used (e.g. ``c\olor``). Defaults to ``True``.
- removed ``Token.literal`` which value is in ``value`` now, normalized value is in ``normalvalue``
- removed ``Token.ESCAPE``. Escapes are contained in IDENTifiers now.
- internal change: WS is generally kept by tokenizer now, former normalized value ``u' '`` is hold in ``Token.normalvalue``. Serializer does not use it yet and some classes (like Selector) use normalvalue.
uses normalized form of @keyword in source CSS if ``True`` (e.g. ``@import``), else literal form in CSS sourcefile (e.g. ``@i\mport``). Defaults to ``True``.
0.9a6
- NEW ``Serializer.prefs.keepcomments`` removes all comments if ``False``, defaults to ``True``
- NEW ``Serializer.prefs.srcatkeyword`` UPDATE see 9.91a1
- fixed tokenizer to handle at least simple escapes like ``c\olor`` which is the same as ``color``. The original value is preserved but not used yet except in CSSComments which preserve the original values. See also Serializer.prefs.srcatkeywords
- ``CSSMediaRule`` tested and lots of bugfixes
* constructor has **no** parameters anymore (``mediaText`` is removed!)
* ``addRule`` is DEPRECATED, use ``insertRule(rule)`` with no index instead.
Synchronized with ``CSSStyleSheet.insertRule``
- setting of ``CSSImportRule.media`` removed, use methods of this object directly.
Synchronized with ``CSSMediaRule.media``
- ``CSSStyleSheet.insertRule`` raises ``xml.dom.IndexSizeErr`` if an invalid index is given. Index may be ``None`` in which case the rule will be appended.
Synchronized with ``CSSMediaRule.insertRule``
- CSSStyleDeclaration bugfixes in parsing invalid tokens
- stylesheets.MediaList bugfixes in parsing uppercase media values like ``PRINT``
- added more unittests (CSSMediaRule)
- various bugfixes
0.9a5 061015
- reimplemented property validator:
- for unknown CSS2 Properties a INFO message is logged
- for invalid CSS2 Property values a WARNING message is issued
- atrules have a new property ``atkeyword`` which is the keyword used in the CSS provided. Normally something like "@import" but may also be an escaped version like "@im\port" or a custom one used in CSSUnknownRule.
- tokenizer and css.selector.Selector
- added CSS3 combinator ``~``
- added CSS3 attribute selectors ``^=``, ``$=``, ``*=``
- added CSS3 pseudo selector ``::`` and pseudo-functions like ``:lang(fr)``
- Token
- added some new constants mainly replacing DELIM, e.g. UNIVERSAL, GREATER, PLUS, TILDE
(CSS3 see http://www.w3.org/TR/css3-selectors)
- Improved parsing of "Unexpected end of string" according to spec
- fixed serializing of CSSUnknownRule if ``valid == False``
- Properties may also be set with a numeric value now, before everything had to be a string. Direct use of _Property is discouraged though as it may well be changed again in a future version.
0.9a4 060927
- CSSStyleSheet:
- removed init parameter ``type`` which is now set as a static type to "text/css"
- removed ``addRule`` which emits DeprecationWarning now
Use ``insertRule`` without parameter ``index``
- added new methods ``setSerializer(cssserializer)`` and
``setSerializerPref(self, pref, value)`` to control output
of a stylesheet directly.
- CSSStyleRule:
- new property ``selectorList`` is an instance of SelectorList
which contains a list of all Selector elements of the rule
- removed ``addSelector()`` and ``getSelectors()``,
use property ``selectorList`` instead
- removed ``getStyleDeclaration()`` and ``setStyleDeclaration()``,
use property ``style`` instead
- CSSStyleDeclaration:
- new constructor parameter ``cssText``
- moved ``SelectorList``, ``Selector`` and ``Property`` to own modules.
Should not be used directly yet anyway.
- Token: renamed ``IMPORTANT`` to ``IMPORTANT_SYM``
- unittests:
- added tests for CSSStyleSheet, CSSStyleRule, SelectorList, Selector
CSSStyleDeclaration, _Property
0.9a3 - 060909
- refined EasyInstall (still some issues to be done)
- CSSCharsetRule serialized and parsed according to spec only as ``@charset "ENCODING";`` so no comments allowed, only one space before encoding string which MUST use ``"`` as delimiter (see http://www.w3.org/TR/CSS21/syndata.html#q23)
NOT COMPLETE YET, E.G. BOM HANDLING
- added tests for setting empty cssText for all @rules and CSSStyleRule
- bugfixes
- CSSStyleDeclaration: Of two Properties if written directly after another``a:1;b:2`` one was swallowed
- CSSSerializer:
added new class cssutils.serialize.Preferences to control output or CSSSerializer
0.9a2 - 060908
- using setuptools for deployment
- new script ``cssparse`` which pprints css "filename"
- subpackages ``css`` and ``stylesheets`` are directly available from ``cssutils`` now
- renamed module ``cssutils.cssparser`` to ``cssutils.parse`` which should not be used directly anyway. Always use ``cssutils.CSSParser`` or ``cssutils.parse`` (s.b)
- added utility functions ``parse(cssText)`` and ``parse(filename, encoding='utf-8')`` to cssutils main package which work like the CSSParser functions with the same name and API
- return value of ``.cssText`` is ``u''`` and not ``None`` if empty now
- serializing
- cssutils.Serializer renamed to cssutils.CSSSerializer to improve usage of
``from cssutils import *``
- cssutils has a property "ser" which is used by all classes to serialize themselves
it is definable with a custom instance of cssutils.Serializer by setting
cssutils.setCSSSerializer(newserializer)
- prefs['CSSImportrule.href format'] may be set to
- 'uri': renders url(...) (default)
- 'string': renders "..."
- None: renders as set in CSSImportRule.hreftype
- css.CSSCharsetRule:
- improved parsing
- fixed API handling (setting of encoding did not work)
- css.CSSImportRule:
- improved parsing
- usage of \*.getFormatted emits DeprecationWarning now and returns \*.cssText
- lots of bugfixes and refactoring of modules, classes
- extension and refactoring of unittests
0.9a1 - 060905 with a new parser (again)
- new tokenizer, complete rewrite
* parses strings and comments
* parses unicode escape sequences (see following)
* emits CSS tokens according to spec (update: not all yet (ESCAPE)!)
- renamed module "comment" to "csscomment" and class "Comment" to "CSSComment"
- configurable Serializer instead of pprint
- reimplemented CSSMediaRule
0.8.x
-----
0.8a6 - 050827
- bugfixes in valuevalidator regarding values of "background-position", thanks to Tim Gerla!
0.8a5 - 050824
- bugfix in css.Comment: if constructor was called with empty or no cssText an exception was raised, reported by Tim Gerla!
- prepared inline comments run through epydoc and generated API docs
0.8a4 - 050814
- csscapture.py
* does download linked, inline and @imported stylesheets now
* renamed csscapture.Capture to csscapture.CSSCapture
* added options, use ``csspapture.py -h`` to view all options
- cssutils.css.CSSStyleSheet defines ``literalCssText`` property if property
``cssText`` is set. This is the unparsed cssText and might be different to cssText
e.g. in case of parser errors.
0.8a3 - 050813
- custom log for CSSparser should work again
- calling script cssparser has 2 new options (not using optparse yet...)
cssparser.py filename.css [encoding[, "debug"]]
1. encoding of the filename.css to parse
2. if called with "debug" debugging mode is enabled and default log prints all messages
- cssutils.css.CSSUnknownRule reintegrated and Tests added
- cssutils.Comment reintegrated
implements css.CSSRule, there a new typevalue COMMENT (=-1) is added
- lexer does handle strings *almost* right now...
- bugfixes
- simplified lexer, still lots of simplification todo
0.8a2 - 050731
- CSSParser may now directly be used from cssutils
cssutils.cssparser as a standalone script does work too.
- css.CSSStyleDeclaration.getPropertyCSSValue(name) implemented
- css.CSSValue updated
- xml.dom.InvalidModificationErr now raised by CSSRule subclasses instead of xml.dom.SyntaxErr in case a non expected rule has been tried to set
- test are updated to the new API and work (not complete and exhaustive though but a bit more than for 0.61)
- bugfixes in some classes due to reanimated tests
- moved module valuevalidator from cssutils.css to cssutils.
Should not be used directly anyway
- split CSSParser in actual CSSParser and utility module used by CSSParser and each css class cssText setting method
- loghandler.ErrorHandler does raiseExceptions by default now. Only CSSParser does overwrite this behaviour. Some tests still need to be looked into...
0.8a1 - 050730
bugfix medialist
medium "projection" was spelled wrong (ended with a space)
docs
new examples and new structure on the website
NEW API **INCOMPATIBLE API CHANGES**
* new package cssutils.css which contains CSS interface implementations (css.CSSStyleSheet, css.CSSRuleList etc)
* new package cssutils.stylesheets which contains Stylesheets interface implementations are in (stylesheets.StyleSheet, stylesheets.MediaList etc)
* module cssutils.cssbuilder has therefor been removed and is replaced by packages cssutils.css and cssutils.stylesheets.
(You may like to define your own cssbuilder module which imports all new classes with their old name if you do not want to change all your code at this time. Usage of the new names is recommended however and there are more subtle changes.)
* CSS interfaces use W3 DOM names normally starting with CSS... now (e.g. CSSStyleSheet)
* CSSStyleSheet now uses superclass stylesheets.StyleSheet
* CSSImportRule is changed to comply to its specification (MediaList is after the URI and not before)
* CSSFontfaceRule (cssutils FontfaceRule) is removed as CSS 2.1 removed this @ rule completely
* CSSProperties is removed. Properties are useful in CSSStyleDeclaration only anyway and are used through that class now.
* some parameters have been renamed to their respective DOM names (e.g. selector is selectorText now in CSSStyleRule constructor
* the API has been cleaned up a bit. Some redundant methods have been removed.
- cssmediarule: removed getRules(), use cssRules property instead
* Comment as a rule is removed currently, might be reintegrated in a future version.
* some classes which have not been implemented fully anyway are not available until they are finished. This is mainly CSSMediaRule (will follow shortly), CSSUnknownRule, CSSValue and other value classes.
0.6.x
-----
0.61 - 050604
bugfix reported and fixed thanks to Matt Harrison:
'border-left-width' property was missing from cssvalues.py
0.60b
tiny internal changes
0.60a
added modules to validate Properties and Values
thanks to Kevin D. Smith
MediaList renamed media type "speech" to "aural"
0.5.x
-----
0.55_52 - 040517 bugfix bugfix release
should do test first ;)
added unittest and fix for fix
0.55_51 - 040517 bugfix release
cssstylesheet.StyleSheet _pprint was renamed to _getCssText but
the call in pprint was not changed...
0.55_5 - 040509
API CHANGES
StyleDeclaration
addProperty made/named private
DEPRECATED anyway, use setProperty
parentRule raises NotImplementedError
RGBColor Implemented
PrimitiveValue uses RGBColor
CSSParser uses setProperty instead of addProperty now
StyleDeclaration, Value, ValueList, PrimitiveValue, RGBcolor
done comparing spec and module docstrings
made list of TODOs
0.55_4 - 040502
implement \*Rule.cssText setting (UnknownRule not complete)
lexer has no log anymore, simply "logs" everything to the
resulting tokenlist
cssstylesheet simplified
bugfixes
0.55_3 not released
cssnormalizer renamed, does not work anyway at the moment
implemented StyleRule.cssText setting
cssproperties.Property has new init param raiseExceptions
similar to the one of CSSParser. does not log yet
and functionality might change as well
* what will not change is that you can specify not
officially specified properties (like moz-opacity etc)
some cleanup in various classes
0.55_2 not released
tests only
0.55_1 not released
API CHANGES
CSSFontFaceRule and CSSPageRule
style is readonly now
NEW
CSSRule
implementation cssText setting
improved docstrings
CSSCharsetRule, CSSFontFaceRule, CSSFontFaceRule, CSSImportRule, CSSSMediaRule, CSSPageRule, CSSStyleRule, CSSUnknownRule
use CSSRule implementation
CSSCharsetRule
uses codecs module to check if valid encoding given
CSSImportRule
new property styleSheet, always None for now
simplified and cleaned up sources
some bugfixes
added tests
test_cssrule
test_csscharsetrule, test_cssfontfacerule, test_cssimportrule,
test_mediarule, test_stylesheetrule, test_unknownrule
subclass test_cssrule now
improved unittests
test_cssstylesheet import problem removed
0.55b not released
start implementation StyleRule.cssText setting
0.54 not released
API CHANGES
Comment.cssText contains comment delimiter
attribute text of Comment private now, renamed to _text
ALPHA new StyleSheet.cssText property (not in W3C DOM)
BUG FIXES
Commentable checked only for str, not unicode. now both
Parser did not raises all errors, might still not do (s. a.)
added unittest for __init__ module
0.53 - 040418
!cssnormalizer does not work in this version - on hold for 1.0
new cssunknownrule.UnknownRule (moved out of module cssrule)
parser now creates Unknown At-Rules in the resulting StyleSheet. they
are no longer just dumped and reported in the parser log.
0.52 - 040414
!cssnormalizer does not work in this version - on hold for 1.0
whitespace in comments will be preserved now
added unittest
0.51 - 040412
!cssnormalizer does not work in this version - on hold for 1.0
API CHANGES
cssrule.SimpleAtRule DEPRECATED and empty
cssmediarule.MediaRule init param "medias" renamed to "media"
use subclasses of CSSRule (CharsetRule, ImportRule,
FontFaceRule or PageRule) instead
StyleRule constructor can be called with arguments (again...)
Comment attribute "comment" renamed to "text"
implemented at least partly almost all DOM Level 2 CSS interfaces now
so the API should be more stable from now on
new statemachine and lexer helper classes for parsing
complete rewrite of CSSParser
CSSParser and lexer put all error messages in a log now
you might give your own log for messages
CSSParser might be configured just to log errors or to raise
xml.dom.DOMExceptions when finding an error
0.4.x
-----
0.41 - 040328
!cssnormalizer does not work in this version - on hold for 1.0
API CHANGES
StyleSheet.getRules() returns a RuleList now
class Selector removed, integrated into Rules now
moved most Classes to own module
StyleSheet, StyleRule, MediaRule, ...
0.40a - 040321
!cssnormalizer does not work in this version
API CHANGES:
cssbuilder.RuleList subclasses list
cssbuilder.Selector moved to cssrules
attribute style of class StyleRule made private (_style)
removed StyleRule.clearStyleDeclaration
attribute selectorlist of class Selector renamed to _selectors and made private
NEW:
MediaList class
moved tests to directory test
made a dist package complete with setup.py
0.3.x
-----
0.31 - 040320
!cssnormalizer does not work in this version
API CHANGES:
StyleDeclaration.addProperty is now DEPRECATED
use StyleDeclaration.setProperty instead
removed CSSParser.pprint(). use CSSParser.getStyleSheet().pprint() instead
(a StyleSheet object had a pprint method anyway)
replaced cssutils own exceptions with standard xml.dom.DOMException
and subclasses
!catch these exceptions instead of CSSException or CSSParserException
moved internal lists (e.g. StyleSheet.nodes list) to private vars
StyleSheet._nodes
!please use methods instead of implementation details
removed cssexception module
removed csscomment module, classes now directly in cssutils
more unittests, start with python cssutils/_test.py
more docs
integrated patches by Cory Dodt for SGML comments and Declaration additions
added some w3c DOM methods
0.30b - 040216
severe API changes
renamed some classes to (almost) DOM names, the CSS prefix of DOM names is ommited though
renamed are
- Stylesheet TO StyleSheet
- Rule TO StyleRule
- AtMediaRule TO MediaRule
- Declaration TO StyleDeclaration
the according methods are renamed as well
class hierarchy is changed as well, please see the example
classes are organized in new modules
0.2.x
-----
0.24_1 - 040214
legal stuff: added licensing information
no files released
0.24 - 040111
split classes in modules, has to be cleaned up again
0.24b - 040106
cleaned up cssbuilder
- Comment now may only contain text
and no comment end delimiter.
(before it had to be a complete css
comment including delimiters)
- AtMediaRule revised completely
validates given media types
new method: addMediaType(media_type)
cssparser updated to new cssbuilder interface and logic
started unittests (v0.0.0.1..., not included yet)
0.23 - 031228
new CSSNormalizer.normalizeDeclarationOrder(stylesheet)
cssbuilder: added methods needed by CSSNormalizer
CSSParser.parse bugfix
0.22 - 031226
CSSParser:
added \n for a declaration ending in addition to ; and }
cssbuilder:
docstrings added for @import and @charset
support build of a selector list in a rule
0.21 - 031226
cleaned up docstrings and added version information
0.20 - 031224
complete rewrite with combination of parser and builder classes
0.1.x
-----
0.10 - 031221
first version to try if i can bring it to work at all
only a prettyprinter included, no builder
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1717516292.8955424
cssutils-2.11.1/PKG-INFO 0000644 0001751 0000177 00000021012 14627634005 014152 0 ustar 00runner docker Metadata-Version: 2.1
Name: cssutils
Version: 2.11.1
Summary: A CSS Cascading Style Sheets library for Python
Author-email: Christof Hoeke
Maintainer-email: "Jason R. Coombs"
Project-URL: Homepage, https://github.com/jaraco/cssutils
Keywords: CSS,Cascading Style Sheets,CSSParser,DOM Level 2 Stylesheets,DOM Level 2 CSS
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: more_itertools
Provides-Extra: test
Requires-Dist: pytest!=8.1.*,>=6; extra == "test"
Requires-Dist: pytest-checkdocs>=2.4; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-mypy; extra == "test"
Requires-Dist: pytest-enabler>=2.2; extra == "test"
Requires-Dist: pytest-ruff>=0.2.1; extra == "test"
Requires-Dist: lxml; python_version < "3.11" and extra == "test"
Requires-Dist: cssselect; extra == "test"
Requires-Dist: importlib_resources; python_version < "3.9" and extra == "test"
Requires-Dist: jaraco.test>=5.1; extra == "test"
Provides-Extra: doc
Requires-Dist: sphinx>=3.5; extra == "doc"
Requires-Dist: jaraco.packaging>=9.3; extra == "doc"
Requires-Dist: rst.linker>=1.9; extra == "doc"
Requires-Dist: furo; extra == "doc"
Requires-Dist: sphinx-lint; extra == "doc"
Requires-Dist: jaraco.tidelift>=1.4; extra == "doc"
.. image:: https://img.shields.io/pypi/v/cssutils.svg
:target: https://pypi.org/project/cssutils
.. image:: https://img.shields.io/pypi/pyversions/cssutils.svg
.. image:: https://github.com/jaraco/cssutils/actions/workflows/main.yml/badge.svg
:target: https://github.com/jaraco/cssutils/actions?query=workflow%3A%22tests%22
:alt: tests
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
:target: https://github.com/astral-sh/ruff
:alt: Ruff
.. image:: https://readthedocs.org/projects/cssutils/badge/?version=latest
:target: https://cssutils.readthedocs.io/en/latest/?badge=latest
.. image:: https://img.shields.io/badge/skeleton-2024-informational
:target: https://blog.jaraco.com/skeleton
.. image:: https://tidelift.com/badges/package/pypi/cssutils
:target: https://tidelift.com/subscription/pkg/pypi-cssutils?utm_source=pypi-cssutils&utm_medium=readme
Overview
========
A Python package to parse and build CSS Cascading Style Sheets. DOM only, not any rendering facilities!
Based upon and partly implementing the following specifications :
`CSS 2.1rev1 `__
General CSS rules and properties are defined here
`CSS3 Module: Syntax `__
Used in parts since cssutils 0.9.4. cssutils tries to use the features from CSS 2.1 and CSS 3 with preference to CSS3 but as this is not final yet some parts are from CSS 2.1
`CSS Fonts Module Level 3 `__
Added changes and additional stuff (since cssutils v0.9.6)
`MediaQueries `__
MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in @import and @media rules.
`Namespaces `__
Added in v0.9.1, updated to definition in CSSOM in v0.9.4, updated in 0.9.5 for dev version
`CSS3 Module: Pages Media `__
Most properties of this spec are implemented including MarginRules
`Selectors `__
The selector syntax defined here (and not in CSS 2.1) should be parsable with cssutils (*should* mind though ;) )
`CSS Backgrounds and Borders Module Level 3 `__, `CSS3 Basic User Interface Module `__, `CSS Text Level 3 `__
Some validation for properties included, mainly `cursor`, `outline`, `resize`, `box-shadow`, `text-shadow`
`Variables `__ / `CSS Custom Properties `__
Experimental specification of CSS Variables which cssutils implements partly. The vars defined in the newer CSS Custom Properties spec should in main parts be at least parsable with cssutils.
`DOM Level 2 Style CSS `__
DOM for package css. 0.9.8 removes support for CSSValue and related API, see PropertyValue and Value API for now
`DOM Level 2 Style Stylesheets `__
DOM for package stylesheets
`CSSOM `__
A few details (mainly the NamespaceRule DOM) are taken from here. Plan is to move implementation to the stuff defined here which is newer but still no REC so might change anytime...
The cssutils tokenizer is a customized implementation of `CSS3 Module: Syntax (W3C Working Draft 13 August 2003) `_ which itself is based on the CSS 2.1 tokenizer. It tries to be as compliant as possible but uses some (helpful) parts of the CSS 2.1 tokenizer.
I guess cssutils is neither CSS 2.1 nor CSS 3 compliant but tries to at least be able to parse both grammars including some more real world cases (some CSS hacks are actually parsed and serialized). Both official grammars are not final nor bugfree but still feasible. cssutils aim is not to be fully compliant to any CSS specification (the specifications seem to be in a constant flow anyway) but cssutils *should* be able to read and write as many as possible CSS stylesheets "in the wild" while at the same time implement the official APIs which are well documented. Some minor extensions are provided as well.
Compatibility
=============
cssutils is developed on modern Python versions. Check the package metadata
for compatibilty.
Beware, cssutils is known to be thread unsafe.
Example
=======
::
import cssutils
css = '''/* a comment with umlaut ä */
@namespace html "http://www.w3.org/1999/xhtml";
@variables { BG: #fff }
html|a { color:red; background: var(BG) }'''
sheet = cssutils.parseString(css)
for rule in sheet:
if rule.type == rule.STYLE_RULE:
# find property
for property in rule.style:
if property.name == 'color':
property.value = 'green'
property.priority = 'IMPORTANT'
break
# or simply:
rule.style['margin'] = '01.0eM' # or: ('1em', 'important')
sheet.encoding = 'ascii'
sheet.namespaces['xhtml'] = 'http://www.w3.org/1999/xhtml'
sheet.namespaces['atom'] = 'http://www.w3.org/2005/Atom'
sheet.add('atom|title {color: #000000 !important}')
sheet.add('@import "sheets/import.css";')
# cssutils.ser.prefs.resolveVariables == True since 0.9.7b2
print sheet.cssText
results in::
@charset "ascii";
@import "sheets/import.css";
/* a comment with umlaut \E4 */
@namespace xhtml "http://www.w3.org/1999/xhtml";
@namespace atom "http://www.w3.org/2005/Atom";
xhtml|a {
color: green !important;
background: #fff;
margin: 1em
}
atom|title {
color: #000 !important
}
Kind Request
============
cssutils is far from being perfect or even complete. If you find any bugs (especially specification violations) or have problems or suggestions please put them in the `Issue Tracker `_.
Thanks
======
Special thanks to Christof Höke for seminal creation of the library.
Thanks to Simon Sapin, Jason R. Coombs, and Walter Doerwald for patches, help and discussion. Thanks to Kevin D. Smith for the value validating module. Thanks also to Cory Dodt, Tim Gerla, James Dobson and Amit Moscovich for helpful suggestions and code patches. Thanks to Fredrik Hedman for help on port of encutils to Python 3.
For Enterprise
==============
Available as part of the Tidelift Subscription.
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
`Learn more `_.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/README.rst 0000644 0001751 0000177 00000015445 14627633762 014572 0 ustar 00runner docker .. image:: https://img.shields.io/pypi/v/cssutils.svg
:target: https://pypi.org/project/cssutils
.. image:: https://img.shields.io/pypi/pyversions/cssutils.svg
.. image:: https://github.com/jaraco/cssutils/actions/workflows/main.yml/badge.svg
:target: https://github.com/jaraco/cssutils/actions?query=workflow%3A%22tests%22
:alt: tests
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
:target: https://github.com/astral-sh/ruff
:alt: Ruff
.. image:: https://readthedocs.org/projects/cssutils/badge/?version=latest
:target: https://cssutils.readthedocs.io/en/latest/?badge=latest
.. image:: https://img.shields.io/badge/skeleton-2024-informational
:target: https://blog.jaraco.com/skeleton
.. image:: https://tidelift.com/badges/package/pypi/cssutils
:target: https://tidelift.com/subscription/pkg/pypi-cssutils?utm_source=pypi-cssutils&utm_medium=readme
Overview
========
A Python package to parse and build CSS Cascading Style Sheets. DOM only, not any rendering facilities!
Based upon and partly implementing the following specifications :
`CSS 2.1rev1 `__
General CSS rules and properties are defined here
`CSS3 Module: Syntax `__
Used in parts since cssutils 0.9.4. cssutils tries to use the features from CSS 2.1 and CSS 3 with preference to CSS3 but as this is not final yet some parts are from CSS 2.1
`CSS Fonts Module Level 3 `__
Added changes and additional stuff (since cssutils v0.9.6)
`MediaQueries `__
MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in @import and @media rules.
`Namespaces `__
Added in v0.9.1, updated to definition in CSSOM in v0.9.4, updated in 0.9.5 for dev version
`CSS3 Module: Pages Media `__
Most properties of this spec are implemented including MarginRules
`Selectors `__
The selector syntax defined here (and not in CSS 2.1) should be parsable with cssutils (*should* mind though ;) )
`CSS Backgrounds and Borders Module Level 3 `__, `CSS3 Basic User Interface Module `__, `CSS Text Level 3 `__
Some validation for properties included, mainly `cursor`, `outline`, `resize`, `box-shadow`, `text-shadow`
`Variables `__ / `CSS Custom Properties `__
Experimental specification of CSS Variables which cssutils implements partly. The vars defined in the newer CSS Custom Properties spec should in main parts be at least parsable with cssutils.
`DOM Level 2 Style CSS `__
DOM for package css. 0.9.8 removes support for CSSValue and related API, see PropertyValue and Value API for now
`DOM Level 2 Style Stylesheets `__
DOM for package stylesheets
`CSSOM `__
A few details (mainly the NamespaceRule DOM) are taken from here. Plan is to move implementation to the stuff defined here which is newer but still no REC so might change anytime...
The cssutils tokenizer is a customized implementation of `CSS3 Module: Syntax (W3C Working Draft 13 August 2003) `_ which itself is based on the CSS 2.1 tokenizer. It tries to be as compliant as possible but uses some (helpful) parts of the CSS 2.1 tokenizer.
I guess cssutils is neither CSS 2.1 nor CSS 3 compliant but tries to at least be able to parse both grammars including some more real world cases (some CSS hacks are actually parsed and serialized). Both official grammars are not final nor bugfree but still feasible. cssutils aim is not to be fully compliant to any CSS specification (the specifications seem to be in a constant flow anyway) but cssutils *should* be able to read and write as many as possible CSS stylesheets "in the wild" while at the same time implement the official APIs which are well documented. Some minor extensions are provided as well.
Compatibility
=============
cssutils is developed on modern Python versions. Check the package metadata
for compatibilty.
Beware, cssutils is known to be thread unsafe.
Example
=======
::
import cssutils
css = '''/* a comment with umlaut ä */
@namespace html "http://www.w3.org/1999/xhtml";
@variables { BG: #fff }
html|a { color:red; background: var(BG) }'''
sheet = cssutils.parseString(css)
for rule in sheet:
if rule.type == rule.STYLE_RULE:
# find property
for property in rule.style:
if property.name == 'color':
property.value = 'green'
property.priority = 'IMPORTANT'
break
# or simply:
rule.style['margin'] = '01.0eM' # or: ('1em', 'important')
sheet.encoding = 'ascii'
sheet.namespaces['xhtml'] = 'http://www.w3.org/1999/xhtml'
sheet.namespaces['atom'] = 'http://www.w3.org/2005/Atom'
sheet.add('atom|title {color: #000000 !important}')
sheet.add('@import "sheets/import.css";')
# cssutils.ser.prefs.resolveVariables == True since 0.9.7b2
print sheet.cssText
results in::
@charset "ascii";
@import "sheets/import.css";
/* a comment with umlaut \E4 */
@namespace xhtml "http://www.w3.org/1999/xhtml";
@namespace atom "http://www.w3.org/2005/Atom";
xhtml|a {
color: green !important;
background: #fff;
margin: 1em
}
atom|title {
color: #000 !important
}
Kind Request
============
cssutils is far from being perfect or even complete. If you find any bugs (especially specification violations) or have problems or suggestions please put them in the `Issue Tracker `_.
Thanks
======
Special thanks to Christof Höke for seminal creation of the library.
Thanks to Simon Sapin, Jason R. Coombs, and Walter Doerwald for patches, help and discussion. Thanks to Kevin D. Smith for the value validating module. Thanks also to Cory Dodt, Tim Gerla, James Dobson and Amit Moscovich for helpful suggestions and code patches. Thanks to Fredrik Hedman for help on port of encutils to Python 3.
For Enterprise
==============
Available as part of the Tidelift Subscription.
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
`Learn more `_.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/SECURITY.md 0000644 0001751 0000177 00000000264 14627633762 014665 0 ustar 00runner docker # Security Contact
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/conftest.py 0000644 0001751 0000177 00000001546 14627633762 015277 0 ustar 00runner docker import importlib
import pytest
import cssutils
collect_ignore = [
'cssutils/_fetchgae.py',
'tools',
]
try:
importlib.import_module('lxml.etree')
except ImportError:
collect_ignore += ['examples/style.py']
@pytest.fixture(autouse=True)
def hermetic_profiles():
"""
Ensure that tests are hermetic w.r.t. profiles.
"""
before = list(cssutils.profile.profiles)
yield
assert before == cssutils.profile.profiles
@pytest.fixture
def saved_profiles(monkeypatch):
profiles = cssutils.profiles.Profiles(log=cssutils.log)
monkeypatch.setattr(cssutils, 'profile', profiles)
@pytest.fixture(autouse=True)
def raise_exceptions():
# configure log to raise exceptions
cssutils.log.raiseExceptions = True
@pytest.fixture(autouse=True)
def restore_serializer_preference_defaults():
cssutils.ser.prefs.useDefaults()
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1717516292.835542
cssutils-2.11.1/cssutils/ 0000755 0001751 0000177 00000000000 14627634005 014732 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/__init__.py 0000644 0001751 0000177 00000033155 14627633762 017063 0 ustar 00runner docker """
CSS Cascading Style Sheets library for Python
A Python package to parse and build CSS Cascading Style Sheets. DOM only, not
any rendering facilities!
Based upon and partly implementing the following specifications :
`CSS 2.1 `__
General CSS rules and properties are defined here
`CSS 2.1 Errata
`__
A few errata, mainly the definition of CHARSET_SYM tokens
`CSS3 Module: Syntax `__
Used in parts since cssutils 0.9.4. cssutils tries to use the features from
CSS 2.1 and CSS 3 with preference to CSS3 but as this is not final yet some
parts are from CSS 2.1
`MediaQueries `__
MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in
@import and @media rules.
`Namespaces `__
Added in v0.9.1, updated to definition in CSSOM in v0.9.4, updated in 0.9.5
for dev version
`CSS3 Module: Pages Media `__
Most properties of this spec are implemented including MarginRules
`Selectors `__
The selector syntax defined here (and not in CSS 2.1) should be parsable
with cssutils (*should* mind though ;) )
`DOM Level 2 Style CSS `__
DOM for package css. 0.9.8 removes support for CSSValue and related API,
see PropertyValue and Value API for now
`DOM Level 2 Style Stylesheets
`__
DOM for package stylesheets
`CSSOM `__
A few details (mainly the NamespaceRule DOM) is taken from here. Plan is
to move implementation to the stuff defined here which is newer but still
no REC so might change anytime...
The cssutils tokenizer is a customized implementation of `CSS3 Module: Syntax
(W3C Working Draft 13 August 2003) `__ which
itself is based on the CSS 2.1 tokenizer. It tries to be as compliant as
possible but uses some (helpful) parts of the CSS 2.1 tokenizer.
I guess cssutils is neither CSS 2.1 nor CSS 3 compliant but tries to at least
be able to parse both grammars including some more real world cases (some CSS
hacks are actually parsed and serialized). Both official grammars are not final
nor bugfree but still feasible. cssutils aim is not to be fully compliant to
any CSS specification (the specifications seem to be in a constant flow anyway)
but cssutils *should* be able to read and write as many as possible CSS
stylesheets "in the wild" while at the same time implement the official APIs
which are well documented. Some minor extensions are provided as well.
Please visit https://cssutils.readthedocs.io/ for more details.
Example::
>>> from cssutils import CSSParser
>>> parser = CSSParser()
>>> sheet = parser.parseString('a { color: red}')
# TODO: shouldn't have to decode here
>>> print(sheet.cssText.decode())
a {
color: red
}
"""
import functools
import itertools
import os.path
import urllib.parse
import urllib.request
import xml.dom
from . import css, errorhandler, stylesheets
from .parse import CSSParser
from .profiles import Profiles
from .serialize import CSSSerializer
__all__ = ['css', 'stylesheets', 'CSSParser', 'CSSSerializer']
log = errorhandler.ErrorHandler()
ser = CSSSerializer()
profile = Profiles(log=log)
# used by Selector defining namespace prefix '*'
_ANYNS = -1
class DOMImplementationCSS:
"""This interface allows the DOM user to create a CSSStyleSheet
outside the context of a document. There is no way to associate
the new CSSStyleSheet with a document in DOM Level 2.
This class is its *own factory*, as it is given to
xml.dom.registerDOMImplementation which simply calls it and receives
an instance of this class then.
"""
_features = [
('css', '1.0'),
('css', '2.0'),
('stylesheets', '1.0'),
('stylesheets', '2.0'),
]
def createCSSStyleSheet(self, title, media):
"""
Creates a new CSSStyleSheet.
title of type DOMString
The advisory title. See also the Style Sheet Interfaces
section.
media of type DOMString
The comma-separated list of media associated with the new style
sheet. See also the Style Sheet Interfaces section.
returns
CSSStyleSheet: A new CSS style sheet.
TODO: DOMException
SYNTAX_ERR: Raised if the specified media string value has a
syntax error and is unparsable.
"""
import warnings
warning = (
"Deprecated, see "
"https://web.archive.org/web/20200701035537/"
"https://bitbucket.org/cthedot/cssutils/issues/69#comment-30669799"
)
warnings.warn(warning, DeprecationWarning, stacklevel=2)
return css.CSSStyleSheet(title=title, media=media)
def createDocument(self, *args, **kwargs):
# sometimes cssutils is picked automatically for
# xml.dom.getDOMImplementation, so provide an implementation
# see (https://web.archive.org/web/20200701035537/
# https://bitbucket.org/cthedot/cssutils/issues/69)
import xml.dom.minidom as minidom
return minidom.DOMImplementation().createDocument(*args, **kwargs)
def createDocumentType(self, *args, **kwargs):
# sometimes cssutils is picked automatically for
# xml.dom.getDOMImplementation, so provide an implementation
# see (https://web.archive.org/web/20200701035537/
# https://bitbucket.org/cthedot/cssutils/issues/69)
import xml.dom.minidom as minidom
return minidom.DOMImplementation().createDocumentType(*args, **kwargs)
def hasFeature(self, feature, version):
return (feature.lower(), str(version)) in self._features
xml.dom.registerDOMImplementation('cssutils', DOMImplementationCSS)
def _parser_redirect(name):
def func(*args, **kwargs):
return getattr(CSSParser(), name)(*args, **kwargs)
func.__doc__ = getattr(CSSParser, name).__doc__
func.__qualname__ = func.__name__ = name
return func
parseString = _parser_redirect('parseString')
parseFile = _parser_redirect('parseFile')
parseUrl = _parser_redirect('parseUrl')
parseStyle = _parser_redirect('parseStyle')
def setSerializer(serializer):
"""Set the global serializer used by all class in cssutils."""
globals().update(ser=serializer)
def _style_declarations(base):
"""
Recursively find all CSSStyleDeclarations.
"""
for rule in getattr(base, 'cssRules', ()):
yield from _style_declarations(rule)
if hasattr(base, 'style'):
yield base.style
def getUrls(sheet):
"""Retrieve all ``url(urlstring)`` values (in e.g.
:class:`cssutils.css.CSSImportRule` or ``cssutils.css.CSSValue``
objects of given `sheet`.
:param sheet:
:class:`cssutils.css.CSSStyleSheet` object whose URLs are yielded
This function is a generator. The generated URL values exclude ``url(`` and
``)`` and surrounding single or double quotes.
"""
imports = (rule.href for rule in sheet if rule.type == rule.IMPORT_RULE)
other = (
value.uri
for style in _style_declarations(sheet)
for value in _uri_values(style)
)
return itertools.chain(imports, other)
def _uri_values(style):
return (
value
for prop in style.getProperties(all=True)
for value in prop.propertyValue
if value.type == 'URI'
)
_flatten = itertools.chain.from_iterable
@functools.singledispatch
def replaceUrls(sheet, replacer, ignoreImportRules=False):
"""Replace all URLs in :class:`cssutils.css.CSSImportRule` or
``cssutils.css.CSSValue`` objects of given `sheet`.
:param sheet:
a :class:`cssutils.css.CSSStyleSheet` to be modified in place.
:param replacer:
a function which is called with a single argument `url` which
is the current value of each url() excluding ``url(``, ``)`` and
surrounding (single or double) quotes.
:param ignoreImportRules:
if ``True`` does not call `replacer` with URLs from @import rules.
"""
imports = (
rule
for rule in sheet
if rule.type == rule.IMPORT_RULE and not ignoreImportRules
)
for rule in imports:
rule.href = replacer(rule.href)
for value in _flatten(map(_uri_values, _style_declarations(sheet))):
value.uri = replacer(value.uri)
@replaceUrls.register(css.CSSStyleDeclaration)
def _(style, replacer, ignoreImportRules=False):
"""Replace all URLs in :class:`cssutils.css.CSSImportRule` or
:class:`cssutils.css.CSSValue` objects of given `style`.
:param style:
a :class:`cssutils.css.CSSStyleDeclaration` to be modified in place.
:param replacer:
a function which is called with a single argument `url` which
is the current value of each url() excluding ``url(``, ``)`` and
surrounding (single or double) quotes.
:param ignoreImportRules:
not applicable, ignored.
"""
for value in _uri_values(style):
value.uri = replacer(value.uri)
class Replacer:
"""
A replacer that uses base to return adjusted URLs.
"""
def __init__(self, base):
self.base = self.extract_base(base)
def __call__(self, uri):
scheme, location, path, query, fragment = urllib.parse.urlsplit(uri)
if scheme or location or path.startswith('/'):
# keep anything absolute
return uri
path, filename = os.path.split(path)
combined = os.path.normpath(os.path.join(self.base, path, filename))
return urllib.request.pathname2url(combined)
@staticmethod
def extract_base(uri):
_, _, raw_path, _, _ = urllib.parse.urlsplit(uri)
base_path, _ = os.path.split(raw_path)
return base_path
def resolveImports(sheet, target=None):
"""
Recursively combine all rules in given `sheet` into a `target` sheet.
Attempts to wrap @import rules that use media information into
@media rules, keeping the media information. This approach may not work in
all instances (e.g. if an @import rule itself contains an @import rule
with different media infos or if it contains rules that may not be
used inside an @media block like @namespace rules). In these cases,
the @import rule from the original sheet takes precedence and a WARNING
is issued.
:param sheet:
:class:`cssutils.css.CSSStyleSheet` from which all import rules are
resolved and added to a resulting *flat* sheet.
:param target:
A :class:`cssutils.css.CSSStyleSheet` object that will be the
resulting *flat* sheet if given.
:returns:
:class:`cssutils.css.CSSStyleSheet` with imports resolved.
"""
if not target:
target = css.CSSStyleSheet(
href=sheet.href, media=sheet.media, title=sheet.title
)
for rule in sheet.cssRules:
if rule.type == rule.CHARSET_RULE:
pass
elif rule.type == rule.IMPORT_RULE:
_resolve_import(rule, target)
else:
target.add(rule)
return target
class MediaCombineDisallowed(Exception):
@classmethod
def check(cls, sheet):
"""
Check if rules present that may not be combined with media.
"""
failed = list(itertools.filterfalse(cls._combinable, sheet))
if failed:
raise cls(failed)
@property
def failed(self):
return self.args[0]
def _combinable(rule):
combinable = rule.COMMENT, rule.STYLE_RULE, rule.IMPORT_RULE
return rule.type in combinable
def _resolve_import(rule, target):
log.info('Processing @import %r' % rule.href, neverraise=True)
if not rule.hrefFound:
# keep @import as it is
log.error(
'Cannot get referenced stylesheet %r, keeping rule' % rule.href,
neverraise=True,
)
target.add(rule)
return
# add all rules of @import to current sheet
target.add(css.CSSComment(cssText='/* START @import "%s" */' % rule.href))
try:
# nested imports
importedSheet = resolveImports(rule.styleSheet)
except xml.dom.HierarchyRequestErr as e:
log.warn(
'@import: Cannot resolve target, keeping rule: %s' % e,
neverraise=True,
)
target.add(rule)
return
# adjust relative URI references
log.info('@import: Adjusting paths for %r' % rule.href, neverraise=True)
replaceUrls(importedSheet, Replacer(rule.href), ignoreImportRules=True)
try:
media_proxy = _check_media_proxy(rule, importedSheet)
except MediaCombineDisallowed as exc:
log.warn(
'Cannot combine imported sheet with given media as rules other than '
f'comments or stylerules; found {exc.failed[0]!r}, keeping {rule.cssText}',
neverraise=True,
)
target.add(rule)
return
imp_target = media_proxy or target
for r in importedSheet:
imp_target.add(r)
if media_proxy:
target.add(media_proxy)
def _check_media_proxy(rule, importedSheet):
# might have to wrap rules in @media if media given
if rule.media.mediaText == 'all':
return
MediaCombineDisallowed.check(importedSheet)
# wrap in @media if media is not `all`
log.info(
'@import: Wrapping some rules in @media '
f' to keep media: {rule.media.mediaText}',
neverraise=True,
)
return css.CSSMediaRule(rule.media.mediaText)
if __name__ == '__main__':
print(__doc__)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/_fetch.py 0000644 0001751 0000177 00000003541 14627633762 016550 0 ustar 00runner docker """Default URL reading functions"""
__all__ = ['_defaultFetcher']
import functools
import urllib.error
import urllib.request
try:
from importlib import metadata
except ImportError:
import importlib_metadata as metadata
import encutils
from . import errorhandler
log = errorhandler.ErrorHandler()
@functools.lru_cache
def _get_version():
try:
return metadata.version('cssutils')
except metadata.PackageNotFoundError:
return 'unknown'
def _defaultFetcher(url):
"""Retrieve data from ``url``. cssutils default implementation of fetch
URL function.
Returns ``(encoding, string)`` or ``None``
"""
try:
request = urllib.request.Request(url)
agent = f'cssutils/{_get_version()} (https://pypi.org/project/cssutils)'
request.add_header('User-agent', agent)
res = urllib.request.urlopen(request)
except urllib.error.HTTPError as e:
# http error, e.g. 404, e can be raised
log.warn(f'HTTPError opening url={url}: {e.code} {e.msg}', error=e)
except urllib.error.URLError as e:
# URLError like mailto: or other IO errors, e can be raised
log.warn('URLError, %s' % e.reason, error=e)
except OSError as e:
# e.g if file URL and not found
log.warn(e, error=OSError)
except ValueError as e:
# invalid url, e.g. "1"
log.warn('ValueError, %s' % e.args[0], error=ValueError)
else:
if res:
mimeType, encoding = encutils.getHTTPInfo(res)
if mimeType != 'text/css':
log.error(
'Expected "text/css" mime type for url=%r but found: %r'
% (url, mimeType),
error=ValueError,
)
content = res.read()
if hasattr(res, 'close'):
res.close()
return encoding, content
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/_fetchgae.py 0000644 0001751 0000177 00000004633 14627633762 017230 0 ustar 00runner docker """GAE specific URL reading functions"""
__all__ = ['_defaultFetcher']
import email.message
from google.appengine.api import urlfetch
from . import errorhandler
log = errorhandler.ErrorHandler()
def _parse_header(content_type):
msg = email.message.EmailMessage()
msg['content-type'] = content_type
return msg.get_content_type(), msg['content-type'].params
def _defaultFetcher(url):
"""
uses GoogleAppEngine (GAE)
fetch(url, payload=None, method=GET, headers={}, allow_truncated=False)
Response
content
The body content of the response.
content_was_truncated
True if the allow_truncated parameter to fetch() was True and
the response exceeded the maximum response size. In this case,
the content attribute contains the truncated response.
status_code
The HTTP status code.
headers
The HTTP response headers, as a mapping of names to values.
Exceptions
exception InvalidURLError()
The URL of the request was not a valid URL, or it used an
unsupported method. Only http and https URLs are supported.
exception DownloadError()
There was an error retrieving the data.
This exception is not raised if the server returns an HTTP
error code: In that case, the response data comes back intact,
including the error code.
exception ResponseTooLargeError()
The response data exceeded the maximum allowed size, and the
allow_truncated parameter passed to fetch() was False.
"""
try:
r = urlfetch.fetch(url, method=urlfetch.GET)
except urlfetch.Error as e:
log.warn(f'Error opening url={url!r}: {e}', error=IOError)
return
if r.status_code != 200:
# TODO: 301 etc
log.warn(
f'Error opening url={url!r}: HTTP status {r.status_code}',
error=IOError,
)
return
# find mimetype and encoding
try:
mimetype, params = _parse_header(r.headers['content-type'])
encoding = params['charset']
except KeyError:
mimetype = 'application/octet-stream'
encoding = None
if mimetype != 'text/css':
log.error(
f'Expected "text/css" mime type for url {url!r} but found: {mimetype!r}',
error=ValueError,
)
return encoding, r.content
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/codec.py 0000644 0001751 0000177 00000053407 14627633762 016403 0 ustar 00runner docker """Python codec for CSS."""
import codecs
import functools
import marshal
# We're using bits to store all possible candidate encodings (or variants, i.e.
# we have two bits for the variants of UTF-16 and two for the
# variants of UTF-32).
#
# Prefixes for various CSS encodings
# UTF-8-SIG xEF xBB xBF
# UTF-16 (LE) xFF xFE ~x00|~x00
# UTF-16 (BE) xFE xFF
# UTF-16-LE @ x00 @ x00
# UTF-16-BE x00 @
# UTF-32 (LE) xFF xFE x00 x00
# UTF-32 (BE) x00 x00 xFE xFF
# UTF-32-LE @ x00 x00 x00
# UTF-32-BE x00 x00 x00 @
# CHARSET @ c h a ...
def chars(bytestring):
return "".join(chr(byte) for byte in bytestring)
def detectencoding_str(input, final=False): # noqa: C901
"""
Detect the encoding of the byte string ``input``, which contains the
beginning of a CSS file. This function returns the detected encoding (or
``None`` if it hasn't got enough data), and a flag that indicates whether
that encoding has been detected explicitely or implicitely. To detect the
encoding the first few bytes are used (or if ``input`` is ASCII compatible
and starts with a charset rule the encoding name from the rule). "Explicit"
detection means that the bytes start with a BOM or a charset rule.
If the encoding can't be detected yet, ``None`` is returned as the encoding.
``final`` specifies whether more data will be available in later calls or
not. If ``final`` is true, ``detectencoding_str()`` will never return
``None`` as the encoding.
"""
# A bit for every candidate
CANDIDATE_UTF_8_SIG = 1
CANDIDATE_UTF_16_AS_LE = 2
CANDIDATE_UTF_16_AS_BE = 4
CANDIDATE_UTF_16_LE = 8
CANDIDATE_UTF_16_BE = 16
CANDIDATE_UTF_32_AS_LE = 32
CANDIDATE_UTF_32_AS_BE = 64
CANDIDATE_UTF_32_LE = 128
CANDIDATE_UTF_32_BE = 256
CANDIDATE_CHARSET = 512
candidates = 1023 # all candidates
# input = chars(input)
li = len(input)
if li >= 1:
# Check first byte
c = input[0]
if c != b"\xef"[0]:
candidates &= ~CANDIDATE_UTF_8_SIG
if c != b"\xff"[0]:
candidates &= ~(CANDIDATE_UTF_32_AS_LE | CANDIDATE_UTF_16_AS_LE)
if c != b"\xfe"[0]:
candidates &= ~CANDIDATE_UTF_16_AS_BE
if c != b"@"[0]:
candidates &= ~(
CANDIDATE_UTF_32_LE | CANDIDATE_UTF_16_LE | CANDIDATE_CHARSET
)
if c != b"\x00"[0]:
candidates &= ~(
CANDIDATE_UTF_32_AS_BE | CANDIDATE_UTF_32_BE | CANDIDATE_UTF_16_BE
)
if li >= 2:
# Check second byte
c = input[1]
if c != b"\xbb"[0]:
candidates &= ~CANDIDATE_UTF_8_SIG
if c != b"\xfe"[0]:
candidates &= ~(CANDIDATE_UTF_16_AS_LE | CANDIDATE_UTF_32_AS_LE)
if c != b"\xff"[0]:
candidates &= ~CANDIDATE_UTF_16_AS_BE
if c != b"\x00"[0]:
candidates &= ~(
CANDIDATE_UTF_16_LE
| CANDIDATE_UTF_32_AS_BE
| CANDIDATE_UTF_32_LE
| CANDIDATE_UTF_32_BE
)
if c != b"@"[0]:
candidates &= ~CANDIDATE_UTF_16_BE
if c != b"c"[0]:
candidates &= ~CANDIDATE_CHARSET
if li >= 3:
# Check third byte
c = input[2]
if c != b"\xbf"[0]:
candidates &= ~CANDIDATE_UTF_8_SIG
if c != b"c"[0]:
candidates &= ~CANDIDATE_UTF_16_LE
if c != b"\x00"[0]:
candidates &= ~(
CANDIDATE_UTF_32_AS_LE
| CANDIDATE_UTF_32_LE
| CANDIDATE_UTF_32_BE
)
if c != b"\xfe"[0]:
candidates &= ~CANDIDATE_UTF_32_AS_BE
if c != b"h"[0]:
candidates &= ~CANDIDATE_CHARSET
if li >= 4:
# Check fourth byte
c = input[3]
if input[2:4] == b"\x00\x00"[0:2]:
candidates &= ~CANDIDATE_UTF_16_AS_LE
if c != b"\x00"[0]:
candidates &= ~(
CANDIDATE_UTF_16_LE
| CANDIDATE_UTF_32_AS_LE
| CANDIDATE_UTF_32_LE
)
if c != b"\xff"[0]:
candidates &= ~CANDIDATE_UTF_32_AS_BE
if c != b"@"[0]:
candidates &= ~CANDIDATE_UTF_32_BE
if c != b"a"[0]:
candidates &= ~CANDIDATE_CHARSET
if candidates == 0:
return ("utf-8", False)
if not (candidates & (candidates - 1)): # only one candidate remaining
if candidates == CANDIDATE_UTF_8_SIG and li >= 3:
return ("utf-8-sig", True)
elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2:
return ("utf-16", True)
elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2:
return ("utf-16", True)
elif candidates == CANDIDATE_UTF_16_LE and li >= 4:
return ("utf-16-le", False)
elif candidates == CANDIDATE_UTF_16_BE and li >= 2:
return ("utf-16-be", False)
elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4:
return ("utf-32", True)
elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4:
return ("utf-32", True)
elif candidates == CANDIDATE_UTF_32_LE and li >= 4:
return ("utf-32-le", False)
elif candidates == CANDIDATE_UTF_32_BE and li >= 4:
return ("utf-32-be", False)
elif candidates == CANDIDATE_CHARSET and li >= 4:
prefix = '@charset "'
charsinput = chars(input)
if charsinput[: len(prefix)] == prefix:
pos = charsinput.find('"', len(prefix))
if pos >= 0:
# TODO: return str and not bytes!
return (charsinput[len(prefix) : pos], True)
# if this is the last call, and we haven't determined an encoding yet,
# we default to UTF-8
if final:
return ("utf-8", False)
return (None, False) # dont' know yet
def detectencoding_unicode(input, final=False):
"""
Detect the encoding of the unicode string ``input``, which contains the
beginning of a CSS file. The encoding is detected from the charset rule
at the beginning of ``input``. If there is no charset rule, ``"utf-8"``
will be returned.
If the encoding can't be detected yet, ``None`` is returned. ``final``
specifies whether more data will be available in later calls or not. If
``final`` is true, ``detectencoding_unicode()`` will never return ``None``.
"""
prefix = '@charset "'
if input.startswith(prefix):
pos = input.find('"', len(prefix))
if pos >= 0:
return (input[len(prefix) : pos], True)
elif final or not prefix.startswith(input):
# if this is the last call, and we haven't determined an encoding yet,
# (or the string definitely doesn't start with prefix) we default to UTF-8
return ("utf-8", False)
return (None, False) # don't know yet
def _fixencoding(input, encoding, final=False):
"""
Replace the name of the encoding in the charset rule at the beginning of
``input`` with ``encoding``. If ``input`` doesn't starts with a charset
rule, ``input`` will be returned unmodified.
If the encoding can't be found yet, ``None`` is returned. ``final``
specifies whether more data will be available in later calls or not.
If ``final`` is true, ``_fixencoding()`` will never return ``None``.
"""
prefix = '@charset "'
if len(input) > len(prefix):
if input.startswith(prefix):
pos = input.find('"', len(prefix))
if pos >= 0:
if encoding.replace("_", "-").lower() == "utf-8-sig":
encoding = "utf-8"
return prefix + encoding + input[pos:]
# we haven't seen the end of the encoding name yet => fall through
else:
return input # doesn't start with prefix, so nothing to fix
elif not prefix.startswith(input) or final:
# can't turn out to be a @charset rule later (or there is no "later")
return input
if final:
return input
return None # don't know yet
def decode(input, errors="strict", encoding=None, force=True):
try:
# py 3 only, memory?! object to bytes
input = input.tobytes()
except AttributeError:
pass
if encoding is None or not force:
(_encoding, explicit) = detectencoding_str(input, True)
if _encoding == "css":
raise ValueError("css not allowed as encoding name")
if (
explicit and not force
) or encoding is None: # Take the encoding from the input
encoding = _encoding
# NEEDS: change in parse.py (str to bytes!)
(input, consumed) = codecs.getdecoder(encoding)(input, errors)
return (_fixencoding(input, str(encoding), True), consumed)
def encode(input, errors="strict", encoding=None):
consumed = len(input)
if encoding is None:
encoding = detectencoding_unicode(input, True)[0]
if encoding.replace("_", "-").lower() == "utf-8-sig":
input = _fixencoding(input, "utf-8", True)
else:
input = _fixencoding(input, str(encoding), True)
if encoding == "css":
raise ValueError("css not allowed as encoding name")
encoder = codecs.getencoder(encoding)
return (encoder(input, errors)[0], consumed)
def _bytes2int(bytes):
# Helper: convert an 8 bit string into an ``int``.
i = 0
for byte in bytes:
i = (i << 8) + ord(byte)
return i
def _int2bytes(i):
# Helper: convert an ``int`` into an 8-bit string.
v = []
while i:
v.insert(0, chr(i & 0xFF))
i >>= 8
return "".join(v)
class IncrementalDecoder(codecs.IncrementalDecoder):
def __init__(self, errors="strict", encoding=None, force=True):
self.decoder = None
self.encoding = encoding
self.force = force
codecs.IncrementalDecoder.__init__(self, errors)
# Store ``errors`` somewhere else,
# because we have to hide it in a property
self._errors = errors
self.buffer = b""
self.headerfixed = False
def iterdecode(self, input):
for part in input:
result = self.decode(part, False)
if result:
yield result
result = self.decode("", True)
if result:
yield result
def decode(self, input, final=False):
# We're doing basically the same as a ``BufferedIncrementalDecoder``,
# but since the buffer is only relevant until the encoding has been
# detected (in which case the buffer of the underlying codec might
# kick in), we're implementing buffering ourselves to avoid some
# overhead.
if self.decoder is None:
input = self.buffer + input
# Do we have to detect the encoding from the input?
if self.encoding is None or not self.force:
(encoding, explicit) = detectencoding_str(input, final)
if encoding is None: # no encoding determined yet
self.buffer = input # retry the complete input on the next call
return "" # no encoding determined yet, so no output
elif encoding == "css":
raise ValueError("css not allowed as encoding name")
if (
explicit and not self.force
) or self.encoding is None: # Take the encoding from the input
self.encoding = encoding
self.buffer = "" # drop buffer, as the decoder might keep its own
decoder = codecs.getincrementaldecoder(self.encoding)
self.decoder = decoder(self._errors)
if self.headerfixed:
return self.decoder.decode(input, final)
# If we haven't fixed the header yet,
# the content of ``self.buffer`` is a ``unicode`` object
output = self.buffer + self.decoder.decode(input, final)
encoding = self.encoding
if encoding.replace("_", "-").lower() == "utf-8-sig":
encoding = "utf-8"
newoutput = _fixencoding(output, str(encoding), final)
if newoutput is None:
# retry fixing the @charset rule (but keep the decoded stuff)
self.buffer = output
return ""
self.headerfixed = True
return newoutput
def reset(self):
codecs.IncrementalDecoder.reset(self)
self.decoder = None
self.buffer = b""
self.headerfixed = False
def _geterrors(self):
return self._errors
def _seterrors(self, errors):
# Setting ``errors`` must be done on the real decoder too
if self.decoder is not None:
self.decoder.errors = errors
self._errors = errors
errors = property(_geterrors, _seterrors)
def getstate(self):
if self.decoder is not None:
state = (
self.encoding,
self.buffer,
self.headerfixed,
True,
self.decoder.getstate(),
)
else:
state = (self.encoding, self.buffer, self.headerfixed, False, None)
return ("", _bytes2int(marshal.dumps(state)))
def setstate(self, state):
state = _int2bytes(marshal.loads(state[1])) # ignore buffered input
self.encoding = state[0]
self.buffer = state[1]
self.headerfixed = state[2]
if state[3] is not None:
self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors)
self.decoder.setstate(state[4])
else:
self.decoder = None
class IncrementalEncoder(codecs.IncrementalEncoder):
def __init__(self, errors="strict", encoding=None):
self.encoder = None
self.encoding = encoding
codecs.IncrementalEncoder.__init__(self, errors)
# Store ``errors`` somewhere else,
# because we have to hide it in a property
self._errors = errors
self.buffer = ""
def iterencode(self, input):
for part in input:
result = self.encode(part, False)
if result:
yield result
result = self.encode("", True)
if result:
yield result
def encode(self, input, final=False):
if self.encoder is None:
input = self.buffer + input
if self.encoding is not None:
# Replace encoding in the @charset rule with the specified one
encoding = self.encoding
if encoding.replace("_", "-").lower() == "utf-8-sig":
encoding = "utf-8"
newinput = _fixencoding(input, str(encoding), final)
if newinput is None: # @charset rule incomplete => Retry next time
self.buffer = input
return ""
input = newinput
else:
# Use encoding from the @charset declaration
self.encoding = detectencoding_unicode(input, final)[0]
if self.encoding is not None:
if self.encoding == "css":
raise ValueError("css not allowed as encoding name")
info = codecs.lookup(self.encoding)
encoding = self.encoding
if self.encoding.replace("_", "-").lower() == "utf-8-sig":
input = _fixencoding(input, "utf-8", True)
self.encoder = info.incrementalencoder(self._errors)
self.buffer = ""
else:
self.buffer = input
return ""
return self.encoder.encode(input, final)
def reset(self):
codecs.IncrementalEncoder.reset(self)
self.encoder = None
self.buffer = ""
def _geterrors(self):
return self._errors
def _seterrors(self, errors):
# Setting ``errors ``must be done on the real encoder too
if self.encoder is not None:
self.encoder.errors = errors
self._errors = errors
errors = property(_geterrors, _seterrors)
def getstate(self):
if self.encoder is not None:
state = (self.encoding, self.buffer, True, self.encoder.getstate())
else:
state = (self.encoding, self.buffer, False, None)
return _bytes2int(marshal.dumps(state))
def setstate(self, state):
state = _int2bytes(marshal.loads(state))
self.encoding = state[0]
self.buffer = state[1]
if state[2] is not None:
self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors)
self.encoder.setstate(state[4])
else:
self.encoder = None
class StreamWriter(codecs.StreamWriter):
def __init__(self, stream, errors="strict", encoding=None, header=False):
codecs.StreamWriter.__init__(self, stream, errors)
self.streamwriter = None
self.encoding = encoding
self._errors = errors
self.buffer = ""
def encode(self, input, errors="strict"):
li = len(input)
if self.streamwriter is None:
input = self.buffer + input
li = len(input)
if self.encoding is not None:
# Replace encoding in the @charset rule with the specified one
encoding = self.encoding
if encoding.replace("_", "-").lower() == "utf-8-sig":
encoding = "utf-8"
newinput = _fixencoding(input, str(encoding), False)
if newinput is None: # @charset rule incomplete => Retry next time
self.buffer = input
return ("", 0)
input = newinput
else:
# Use encoding from the @charset declaration
self.encoding = detectencoding_unicode(input, False)[0]
if self.encoding is not None:
if self.encoding == "css":
raise ValueError("css not allowed as encoding name")
self.streamwriter = codecs.getwriter(self.encoding)(
self.stream, self._errors
)
encoding = self.encoding
if self.encoding.replace("_", "-").lower() == "utf-8-sig":
input = _fixencoding(input, "utf-8", True)
self.buffer = ""
else:
self.buffer = input
return ("", 0)
return (self.streamwriter.encode(input, errors)[0], li)
def _geterrors(self):
return self._errors
def _seterrors(self, errors):
# Setting ``errors`` must be done on the streamwriter too
try:
if self.streamwriter is not None:
self.streamwriter.errors = errors
except AttributeError:
# TODO: py3 only exception?
pass
self._errors = errors
errors = property(_geterrors, _seterrors)
class StreamReader(codecs.StreamReader):
def __init__(self, stream, errors="strict", encoding=None, force=True):
codecs.StreamReader.__init__(self, stream, errors)
self.streamreader = None
self.encoding = encoding
self.force = force
self._errors = errors
def decode(self, input, errors="strict"):
if self.streamreader is None:
if self.encoding is None or not self.force:
(encoding, explicit) = detectencoding_str(input, False)
if encoding is None: # no encoding determined yet
return ("", 0) # no encoding determined yet, so no output
elif encoding == "css":
raise ValueError("css not allowed as encoding name")
if (
explicit and not self.force
) or self.encoding is None: # Take the encoding from the input
self.encoding = encoding
streamreader = codecs.getreader(self.encoding)
streamreader = streamreader(self.stream, self._errors)
(output, consumed) = streamreader.decode(input, errors)
encoding = self.encoding
if encoding.replace("_", "-").lower() == "utf-8-sig":
encoding = "utf-8"
newoutput = _fixencoding(output, str(encoding), False)
if newoutput is not None:
self.streamreader = streamreader
return (newoutput, consumed)
return ("", 0) # we will create a new streamreader on the next call
return self.streamreader.decode(input, errors)
def _geterrors(self):
return self._errors
def _seterrors(self, errors):
# Setting ``errors`` must be done on the streamreader too
try:
if self.streamreader is not None:
self.streamreader.errors = errors
except AttributeError:
# TODO: py3 only exception?
pass
self._errors = errors
errors = property(_geterrors, _seterrors)
@codecs.register
def search_function(name):
if name != "css":
return
return codecs.CodecInfo(
name="css",
encode=encode,
decode=decode,
incrementalencoder=IncrementalEncoder,
incrementaldecoder=IncrementalDecoder,
streamwriter=StreamWriter,
streamreader=StreamReader,
)
@functools.partial(codecs.register_error, "cssescape")
def cssescape(exc):
"""
Error handler for CSS escaping.
"""
if not isinstance(exc, UnicodeEncodeError):
raise TypeError("don't know how to handle %r" % exc)
return (
"".join("\\%06x" % ord(c) for c in exc.object[exc.start : exc.end]),
exc.end,
)
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1717516292.839542
cssutils-2.11.1/cssutils/css/ 0000755 0001751 0000177 00000000000 14627634005 015522 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/__init__.py 0000644 0001751 0000177 00000004115 14627633762 017645 0 ustar 00runner docker """Implements Document Object Model Level 2 CSS
http://www.w3.org/TR/2000/PR-DOM-Level-2-Style-20000927/css.html
currently implemented
- CSSStyleSheet
- CSSRuleList
- CSSRule
- CSSComment (cssutils addon)
- CSSCharsetRule
- CSSFontFaceRule
- CSSImportRule
- CSSMediaRule
- CSSNamespaceRule (WD)
- CSSPageRule
- CSSStyleRule
- CSSUnkownRule
- Selector and SelectorList
- CSSStyleDeclaration
- CSS2Properties
- CSSValue
- CSSPrimitiveValue
- CSSValueList
- CSSVariablesRule
- CSSVariablesDeclaration
todo
- RGBColor, Rect, Counter
"""
__all__ = [
'CSSStyleSheet',
'CSSRuleList',
'CSSRule',
'CSSComment',
'CSSCharsetRule',
'CSSFontFaceRule',
'CSSImportRule',
'CSSMediaRule',
'CSSNamespaceRule',
'CSSPageRule',
'MarginRule',
'CSSStyleRule',
'CSSUnknownRule',
'CSSVariablesRule',
'CSSVariablesDeclaration',
'Selector',
'SelectorList',
'CSSStyleDeclaration',
'Property',
'PropertyValue',
'Value',
'ColorValue',
'DimensionValue',
'URIValue',
'CSSFunction',
'CSSVariable',
'MSValue',
'CSSCalc',
]
from .csscharsetrule import CSSCharsetRule
from .csscomment import CSSComment
from .cssfontfacerule import CSSFontFaceRule
from .cssimportrule import CSSImportRule
from .cssmediarule import CSSMediaRule
from .cssnamespacerule import CSSNamespaceRule
from .csspagerule import CSSPageRule
from .cssrule import CSSRule
from .cssrulelist import CSSRuleList
from .cssstyledeclaration import CSSStyleDeclaration
from .cssstylerule import CSSStyleRule
from .cssstylesheet import CSSStyleSheet
from .cssunknownrule import CSSUnknownRule
from .cssvariablesdeclaration import CSSVariablesDeclaration
from .cssvariablesrule import CSSVariablesRule
from .marginrule import MarginRule
from .property import Property
from .selector import Selector
from .selectorlist import SelectorList
from .value import (
ColorValue,
CSSCalc,
CSSFunction,
CSSVariable,
DimensionValue,
MSValue,
PropertyValue,
URIValue,
Value,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/colors.py 0000644 0001751 0000177 00000013467 14627633762 017421 0 ustar 00runner docker """
Built from something like this:
print [
(
row[2].text_content().strip(),
eval(row[4].text_content().strip())
)
for row in lxml.html.parse('http://www.w3.org/TR/css3-color/')
.xpath("//*[@class='colortable']//tr[position()>1]")
]
by Simon Sapin
"""
COLORS = {
'transparent': (0, 0, 0, 0.0),
'black': (0, 0, 0, 1.0),
'silver': (192, 192, 192, 1.0),
'gray': (128, 128, 128, 1.0),
'white': (255, 255, 255, 1.0),
'maroon': (128, 0, 0, 1.0),
'red': (255, 0, 0, 1.0),
'purple': (128, 0, 128, 1.0),
'fuchsia': (255, 0, 255, 1.0),
'green': (0, 128, 0, 1.0),
'lime': (0, 255, 0, 1.0),
'olive': (128, 128, 0, 1.0),
'yellow': (255, 255, 0, 1.0),
'navy': (0, 0, 128, 1.0),
'blue': (0, 0, 255, 1.0),
'teal': (0, 128, 128, 1.0),
'aqua': (0, 255, 255, 1.0),
'aliceblue': (240, 248, 255, 1.0),
'antiquewhite': (250, 235, 215, 1.0),
'aquamarine': (127, 255, 212, 1.0),
'azure': (240, 255, 255, 1.0),
'beige': (245, 245, 220, 1.0),
'bisque': (255, 228, 196, 1.0),
'blanchedalmond': (255, 235, 205, 1.0),
'blueviolet': (138, 43, 226, 1.0),
'brown': (165, 42, 42, 1.0),
'burlywood': (222, 184, 135, 1.0),
'cadetblue': (95, 158, 160, 1.0),
'chartreuse': (127, 255, 0, 1.0),
'chocolate': (210, 105, 30, 1.0),
'coral': (255, 127, 80, 1.0),
'cornflowerblue': (100, 149, 237, 1.0),
'cornsilk': (255, 248, 220, 1.0),
'crimson': (220, 20, 60, 1.0),
'cyan': (0, 255, 255, 1.0),
'darkblue': (0, 0, 139, 1.0),
'darkcyan': (0, 139, 139, 1.0),
'darkgoldenrod': (184, 134, 11, 1.0),
'darkgray': (169, 169, 169, 1.0),
'darkgreen': (0, 100, 0, 1.0),
'darkgrey': (169, 169, 169, 1.0),
'darkkhaki': (189, 183, 107, 1.0),
'darkmagenta': (139, 0, 139, 1.0),
'darkolivegreen': (85, 107, 47, 1.0),
'darkorange': (255, 140, 0, 1.0),
'darkorchid': (153, 50, 204, 1.0),
'darkred': (139, 0, 0, 1.0),
'darksalmon': (233, 150, 122, 1.0),
'darkseagreen': (143, 188, 143, 1.0),
'darkslateblue': (72, 61, 139, 1.0),
'darkslategray': (47, 79, 79, 1.0),
'darkslategrey': (47, 79, 79, 1.0),
'darkturquoise': (0, 206, 209, 1.0),
'darkviolet': (148, 0, 211, 1.0),
'deeppink': (255, 20, 147, 1.0),
'deepskyblue': (0, 191, 255, 1.0),
'dimgray': (105, 105, 105, 1.0),
'dimgrey': (105, 105, 105, 1.0),
'dodgerblue': (30, 144, 255, 1.0),
'firebrick': (178, 34, 34, 1.0),
'floralwhite': (255, 250, 240, 1.0),
'forestgreen': (34, 139, 34, 1.0),
'gainsboro': (220, 220, 220, 1.0),
'ghostwhite': (248, 248, 255, 1.0),
'gold': (255, 215, 0, 1.0),
'goldenrod': (218, 165, 32, 1.0),
'greenyellow': (173, 255, 47, 1.0),
'grey': (128, 128, 128, 1.0),
'honeydew': (240, 255, 240, 1.0),
'hotpink': (255, 105, 180, 1.0),
'indianred': (205, 92, 92, 1.0),
'indigo': (75, 0, 130, 1.0),
'ivory': (255, 255, 240, 1.0),
'khaki': (240, 230, 140, 1.0),
'lavender': (230, 230, 250, 1.0),
'lavenderblush': (255, 240, 245, 1.0),
'lawngreen': (124, 252, 0, 1.0),
'lemonchiffon': (255, 250, 205, 1.0),
'lightblue': (173, 216, 230, 1.0),
'lightcoral': (240, 128, 128, 1.0),
'lightcyan': (224, 255, 255, 1.0),
'lightgoldenrodyellow': (250, 250, 210, 1.0),
'lightgray': (211, 211, 211, 1.0),
'lightgreen': (144, 238, 144, 1.0),
'lightgrey': (211, 211, 211, 1.0),
'lightpink': (255, 182, 193, 1.0),
'lightsalmon': (255, 160, 122, 1.0),
'lightseagreen': (32, 178, 170, 1.0),
'lightskyblue': (135, 206, 250, 1.0),
'lightslategray': (119, 136, 153, 1.0),
'lightslategrey': (119, 136, 153, 1.0),
'lightsteelblue': (176, 196, 222, 1.0),
'lightyellow': (255, 255, 224, 1.0),
'limegreen': (50, 205, 50, 1.0),
'linen': (250, 240, 230, 1.0),
'magenta': (255, 0, 255, 1.0),
'mediumaquamarine': (102, 205, 170, 1.0),
'mediumblue': (0, 0, 205, 1.0),
'mediumorchid': (186, 85, 211, 1.0),
'mediumpurple': (147, 112, 219, 1.0),
'mediumseagreen': (60, 179, 113, 1.0),
'mediumslateblue': (123, 104, 238, 1.0),
'mediumspringgreen': (0, 250, 154, 1.0),
'mediumturquoise': (72, 209, 204, 1.0),
'mediumvioletred': (199, 21, 133, 1.0),
'midnightblue': (25, 25, 112, 1.0),
'mintcream': (245, 255, 250, 1.0),
'mistyrose': (255, 228, 225, 1.0),
'moccasin': (255, 228, 181, 1.0),
'navajowhite': (255, 222, 173, 1.0),
'oldlace': (253, 245, 230, 1.0),
'olivedrab': (107, 142, 35, 1.0),
'orange': (255, 165, 0, 1.0),
'orangered': (255, 69, 0, 1.0),
'orchid': (218, 112, 214, 1.0),
'palegoldenrod': (238, 232, 170, 1.0),
'palegreen': (152, 251, 152, 1.0),
'paleturquoise': (175, 238, 238, 1.0),
'palevioletred': (219, 112, 147, 1.0),
'papayawhip': (255, 239, 213, 1.0),
'peachpuff': (255, 218, 185, 1.0),
'peru': (205, 133, 63, 1.0),
'pink': (255, 192, 203, 1.0),
'plum': (221, 160, 221, 1.0),
'powderblue': (176, 224, 230, 1.0),
'rosybrown': (188, 143, 143, 1.0),
'royalblue': (65, 105, 225, 1.0),
'saddlebrown': (139, 69, 19, 1.0),
'salmon': (250, 128, 114, 1.0),
'sandybrown': (244, 164, 96, 1.0),
'seagreen': (46, 139, 87, 1.0),
'seashell': (255, 245, 238, 1.0),
'sienna': (160, 82, 45, 1.0),
'skyblue': (135, 206, 235, 1.0),
'slateblue': (106, 90, 205, 1.0),
'slategray': (112, 128, 144, 1.0),
'slategrey': (112, 128, 144, 1.0),
'snow': (255, 250, 250, 1.0),
'springgreen': (0, 255, 127, 1.0),
'steelblue': (70, 130, 180, 1.0),
'tan': (210, 180, 140, 1.0),
'thistle': (216, 191, 216, 1.0),
'tomato': (255, 99, 71, 1.0),
'turquoise': (64, 224, 208, 1.0),
'violet': (238, 130, 238, 1.0),
'wheat': (245, 222, 179, 1.0),
'whitesmoke': (245, 245, 245, 1.0),
'yellowgreen': (154, 205, 50, 1.0),
}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/csscharsetrule.py 0000644 0001751 0000177 00000013401 14627633762 021136 0 ustar 00runner docker """CSSCharsetRule implements DOM Level 2 CSS CSSCharsetRule."""
__all__ = ['CSSCharsetRule']
import codecs
import xml.dom
import cssutils
from . import cssrule
class CSSCharsetRule(cssrule.CSSRule):
"""
The CSSCharsetRule interface represents an @charset rule in a CSS style
sheet. The value of the encoding attribute does not affect the encoding
of text data in the DOM objects; this encoding is always UTF-16
(also in Python?). After a stylesheet is loaded, the value of the
encoding attribute is the value found in the @charset rule. If there
was no @charset in the original document, then no CSSCharsetRule is
created. The value of the encoding attribute may also be used as a hint
for the encoding used on serialization of the style sheet.
The value of the @charset rule (and therefore of the CSSCharsetRule)
may not correspond to the encoding the document actually came in;
character encoding information e.g. in an HTTP header, has priority
(see CSS document representation) but this is not reflected in the
CSSCharsetRule.
This rule is not really needed anymore as setting
:attr:`CSSStyleSheet.encoding` is much easier.
Format::
charsetrule:
CHARSET_SYM S* STRING S* ';'
BUT: Only valid format is (single space, double quotes!)::
@charset "ENCODING";
"""
def __init__(
self, encoding=None, parentRule=None, parentStyleSheet=None, readonly=False
):
"""
:param encoding:
a valid character encoding
:param readonly:
defaults to False, not used yet
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@charset'
if encoding:
self.encoding = encoding
else:
self._encoding = None
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(encoding={self.encoding!r})"
def __str__(self):
return f""
def _getCssText(self):
"""The parsable textual representation."""
return cssutils.ser.do_CSSCharsetRule(self)
def _setCssText(self, cssText):
"""
:param cssText:
A parsable DOMString.
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
wellformed = True
tokenizer = self._tokenize2(cssText)
if self._type(self._nexttoken(tokenizer)) != self._prods.CHARSET_SYM:
wellformed = False
self._log.error(
'CSSCharsetRule must start with "@charset "',
error=xml.dom.InvalidModificationErr,
)
encodingtoken = self._nexttoken(tokenizer)
encodingtype = self._type(encodingtoken)
encoding = self._stringtokenvalue(encodingtoken)
if self._prods.STRING != encodingtype or not encoding:
wellformed = False
self._log.error(
'CSSCharsetRule: no encoding found; %r.' % self._valuestr(cssText)
)
semicolon = self._tokenvalue(self._nexttoken(tokenizer))
EOFtype = self._type(self._nexttoken(tokenizer))
if ';' != semicolon or EOFtype not in ('EOF', None):
wellformed = False
self._log.error(
'CSSCharsetRule: Syntax Error: %r.' % self._valuestr(cssText)
)
if wellformed:
self.encoding = encoding
cssText = property(
fget=_getCssText,
fset=_setCssText,
doc="(DOM) The parsable textual representation.",
)
def _setEncoding(self, encoding):
"""
:param encoding:
a valid encoding to be used. Currently only valid Python encodings
are allowed.
:exceptions:
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this encoding rule is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified encoding value has a syntax error and
is unparsable.
"""
self._checkReadonly()
tokenizer = self._tokenize2(encoding)
encodingtoken = self._nexttoken(tokenizer)
unexpected = self._nexttoken(tokenizer)
if (
not encodingtoken
or unexpected
or self._prods.IDENT != self._type(encodingtoken)
):
self._log.error(
'CSSCharsetRule: Syntax Error in encoding value ' '%r.' % encoding
)
else:
try:
codecs.lookup(encoding)
except LookupError:
self._log.error(
'CSSCharsetRule: Unknown (Python) encoding %r.' % encoding
)
else:
self._encoding = encoding.lower()
encoding = property(
lambda self: self._encoding,
_setEncoding,
doc="(DOM)The encoding information used in this @charset rule.",
)
type = property(
lambda self: self.CHARSET_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
wellformed = property(lambda self: bool(self.encoding))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/csscomment.py 0000644 0001751 0000177 00000005131 14627633762 020260 0 ustar 00runner docker """CSSComment is not defined in DOM Level 2 at all but a cssutils defined
class only.
Implements CSSRule which is also extended for a CSSComment rule type.
"""
__all__ = ['CSSComment']
import xml.dom
import cssutils
from . import cssrule
class CSSComment(cssrule.CSSRule):
"""
Represents a CSS comment (cssutils only).
Format::
/*...*/
"""
def __init__(
self, cssText=None, parentRule=None, parentStyleSheet=None, readonly=False
):
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._cssText = None
if cssText:
self._setCssText(cssText)
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(cssText={self.cssText!r})"
def __str__(self):
return f""
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSComment(self)
def _setCssText(self, cssText):
"""
:param cssText:
textual text to set or tokenlist which is not tokenized
anymore. May also be a single token for this rule
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
commenttoken = self._nexttoken(tokenizer)
unexpected = self._nexttoken(tokenizer)
if (
not commenttoken
or self._type(commenttoken) != self._prods.COMMENT
or unexpected
):
self._log.error(
'CSSComment: Not a CSSComment: %r' % self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
self._cssText = self._tokenvalue(commenttoken)
cssText = property(
_getCssText,
_setCssText,
doc="The parsable textual representation of this rule.",
)
type = property(
lambda self: self.COMMENT,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
# constant but needed:
wellformed = property(lambda self: True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssfontfacerule.py 0000644 0001751 0000177 00000014420 14627633762 021274 0 ustar 00runner docker """CSSFontFaceRule implements DOM Level 2 CSS CSSFontFaceRule.
From cssutils 0.9.6 additions from CSS Fonts Module Level 3 are
added http://www.w3.org/TR/css3-fonts/.
"""
__all__ = ['CSSFontFaceRule']
import xml.dom
import cssutils
from . import cssrule
from .cssstyledeclaration import CSSStyleDeclaration
class CSSFontFaceRule(cssrule.CSSRule):
"""
The CSSFontFaceRule interface represents a @font-face rule in a CSS
style sheet. The @font-face rule is used to hold a set of font
descriptions.
Format::
font_face
: FONT_FACE_SYM S*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
cssutils uses a :class:`~cssutils.css.CSSStyleDeclaration` to
represent the font descriptions. For validation a specific profile
is used though were some properties have other valid values than
when used in e.g. a :class:`~cssutils.css.CSSStyleRule`.
"""
def __init__(
self, style=None, parentRule=None, parentStyleSheet=None, readonly=False
):
"""
If readonly allows setting of properties in constructor only.
:param style:
CSSStyleDeclaration used to hold any font descriptions
for this CSSFontFaceRule
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@font-face'
if style:
self.style = style
else:
self.style = CSSStyleDeclaration()
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(style={self.style.cssText!r})"
def __str__(self):
return f""
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSFontFaceRule(self)
def _setCssText(self, cssText):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
attoken = self._nexttoken(tokenizer, None)
if self._type(attoken) != self._prods.FONT_FACE_SYM:
self._log.error(
'CSSFontFaceRule: No CSSFontFaceRule found: %s'
% self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
newStyle = CSSStyleDeclaration(parentRule=self)
ok = True
beforetokens, brace = self._tokensupto2(
tokenizer, blockstartonly=True, separateEnd=True
)
if self._tokenvalue(brace) != '{':
ok = False
self._log.error(
'CSSFontFaceRule: No start { of style '
'declaration found: %r' % self._valuestr(cssText),
brace,
)
# parse stuff before { which should be comments and S only
new = {'wellformed': True}
newseq = self._tempSeq()
beforewellformed, expected = self._parse(
expected=':',
seq=newseq,
tokenizer=self._tokenize2(beforetokens),
productions={},
)
ok = ok and beforewellformed and new['wellformed']
styletokens, braceorEOFtoken = self._tokensupto2(
tokenizer, blockendonly=True, separateEnd=True
)
val, type_ = self._tokenvalue(braceorEOFtoken), self._type(braceorEOFtoken)
if val != '}' and type_ != 'EOF':
ok = False
self._log.error(
'CSSFontFaceRule: No "}" after style '
'declaration found: %r' % self._valuestr(cssText)
)
nonetoken = self._nexttoken(tokenizer)
if nonetoken:
ok = False
self._log.error(
'CSSFontFaceRule: Trailing content found.', token=nonetoken
)
if 'EOF' == type_:
# add again as style needs it
styletokens.append(braceorEOFtoken)
# SET, may raise:
newStyle.cssText = styletokens
if ok:
# contains probably comments only (upto ``{``)
self._setSeq(newseq)
self.style = newStyle
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) The parsable textual representation of this " "rule.",
)
def _setStyle(self, style):
"""
:param style:
a CSSStyleDeclaration or string
"""
self._checkReadonly()
if isinstance(style, str):
self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
else:
style._parentRule = self
self._style = style
style = property(
lambda self: self._style,
_setStyle,
doc="(DOM) The declaration-block of this rule set, "
"a :class:`~cssutils.css.CSSStyleDeclaration`.",
)
type = property(
lambda self: self.FONT_FACE_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
def _getValid(self):
needed = ['font-family', 'src']
for p in self.style.getProperties(all=True):
if not p.valid:
return False
try:
needed.remove(p.name)
except ValueError:
pass
return not bool(needed)
valid = property(
_getValid,
doc="CSSFontFace is valid if properties `font-family` "
"and `src` are set and all properties are valid.",
)
# constant but needed:
wellformed = property(lambda self: True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssimportrule.py 0000644 0001751 0000177 00000034263 14627633762 021030 0 ustar 00runner docker """CSSImportRule implements DOM Level 2 CSS CSSImportRule plus the
``name`` property from http://www.w3.org/TR/css3-cascade/#cascading."""
__all__ = ['CSSImportRule']
import os
import urllib.parse
import xml.dom
import cssutils
from . import cssrule
class CSSImportRule(cssrule.CSSRule):
"""
Represents an @import rule within a CSS style sheet. The @import rule
is used to import style rules from other style sheets.
Format::
import
: IMPORT_SYM S*
[STRING|URI] S* [ medium [ COMMA S* medium]* ]? S* STRING? S* ';' S*
;
"""
def __init__(
self,
href=None,
mediaText=None,
name=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""
If readonly allows setting of properties in constructor only
:param href:
location of the style sheet to be imported.
:param mediaText:
A list of media types for which this style sheet may be used
as a string
:param name:
Additional name of imported style sheet
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@import'
self._styleSheet = None
# string or uri used for reserialization
self.hreftype = None
# prepare seq
seq = self._tempSeq()
seq.append(None, 'href')
# seq.append(None, 'media')
seq.append(None, 'name')
self._setSeq(seq)
# 1. media
if mediaText:
self.media = mediaText
else:
# must be all for @import
self.media = cssutils.stylesheets.MediaList(mediaText='all')
# 2. name
self.name = name
# 3. href and styleSheet
self.href = href
self._readonly = readonly
def __repr__(self):
if self._usemedia:
mediaText = self.media.mediaText
else:
mediaText = None
return f"cssutils.css.{self.__class__.__name__}(href={self.href!r}, mediaText={mediaText!r}, name={self.name!r})"
def __str__(self):
if self._usemedia:
mediaText = self.media.mediaText
else:
mediaText = None
return f""
_usemedia = property(
lambda self: self.media.mediaText not in ('', 'all'),
doc="if self.media is used (or simply empty)",
)
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSImportRule(self)
def _setCssText(self, cssText): # noqa: C901
"""
:exceptions:
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
attoken = self._nexttoken(tokenizer, None)
if self._type(attoken) != self._prods.IMPORT_SYM:
self._log.error(
'CSSImportRule: No CSSImportRule found: %s' % self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
# for closures: must be a mutable
new = {
'keyword': self._tokenvalue(attoken),
'href': None,
'hreftype': None,
'media': None,
'name': None,
'wellformed': True,
}
def __doname(seq, token):
# called by _string or _ident
new['name'] = self._stringtokenvalue(token)
seq.append(new['name'], 'name')
return ';'
def _string(expected, seq, token, tokenizer=None):
if 'href' == expected:
# href
new['href'] = self._stringtokenvalue(token)
new['hreftype'] = 'string'
seq.append(new['href'], 'href')
return 'media name ;'
elif 'name' in expected:
# name
return __doname(seq, token)
else:
new['wellformed'] = False
self._log.error('CSSImportRule: Unexpected string.', token)
return expected
def _uri(expected, seq, token, tokenizer=None):
# href
if 'href' == expected:
uri = self._uritokenvalue(token)
new['hreftype'] = 'uri'
new['href'] = uri
seq.append(new['href'], 'href')
return 'media name ;'
else:
new['wellformed'] = False
self._log.error('CSSImportRule: Unexpected URI.', token)
return expected
def _ident(expected, seq, token, tokenizer=None):
# medialist ending with ; which is checked upon too
if expected.startswith('media'):
mediatokens = self._tokensupto2(
tokenizer, importmediaqueryendonly=True
)
mediatokens.insert(0, token) # push found token
last = mediatokens.pop() # retrieve ;
lastval, lasttyp = self._tokenvalue(last), self._type(last)
if lastval != ';' and lasttyp not in ('EOF', self._prods.STRING):
new['wellformed'] = False
self._log.error(
'CSSImportRule: No ";" found: %s' % self._valuestr(cssText),
token=token,
)
newMedia = cssutils.stylesheets.MediaList(parentRule=self)
newMedia.mediaText = mediatokens
if newMedia.wellformed:
new['media'] = newMedia
seq.append(newMedia, 'media')
else:
new['wellformed'] = False
self._log.error(
'CSSImportRule: Invalid MediaList: %s'
% self._valuestr(cssText),
token=token,
)
if lasttyp == self._prods.STRING:
# name
return __doname(seq, last)
else:
return 'EOF' # ';' is token "last"
else:
new['wellformed'] = False
self._log.error('CSSImportRule: Unexpected ident.', token)
return expected
def _char(expected, seq, token, tokenizer=None):
# final ;
val = self._tokenvalue(token)
if expected.endswith(';') and ';' == val:
return 'EOF'
else:
new['wellformed'] = False
self._log.error('CSSImportRule: Unexpected char.', token)
return expected
# import : IMPORT_SYM S* [STRING|URI]
# S* [ medium [ ',' S* medium]* ]? ';' S*
# STRING? # see http://www.w3.org/TR/css3-cascade/#cascading
# ;
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected='href',
seq=newseq,
tokenizer=tokenizer,
productions={
'STRING': _string,
'URI': _uri,
'IDENT': _ident,
'CHAR': _char,
},
new=new,
)
# wellformed set by parse
ok = wellformed and new['wellformed']
# post conditions
if not new['href']:
ok = False
self._log.error(
'CSSImportRule: No href found: %s' % self._valuestr(cssText)
)
if expected != 'EOF':
ok = False
self._log.error(
'CSSImportRule: No ";" found: %s' % self._valuestr(cssText)
)
# set all
if ok:
self._setSeq(newseq)
self.atkeyword = new['keyword']
self.hreftype = new['hreftype']
self.name = new['name']
if new['media']:
self.media = new['media']
else:
# must be all for @import
self.media = cssutils.stylesheets.MediaList(mediaText='all')
# needs new self.media
self.href = new['href']
cssText = property(
fget=_getCssText,
fset=_setCssText,
doc="(DOM) The parsable textual representation of this rule.",
)
def _setHref(self, href):
# set new href
self._href = href
# update seq
for i, item in enumerate(self.seq):
type_ = item.type
if 'href' == type_:
self._seq[i] = (href, type_, item.line, item.col)
break
importedSheet = cssutils.css.CSSStyleSheet(
media=self.media, ownerRule=self, title=self.name
)
self.hrefFound = False
# set styleSheet
if href and self.parentStyleSheet:
# loading errors are all catched!
# relative href
parentHref = self.parentStyleSheet.href
if parentHref is None:
# use cwd instead
parentHref = cssutils.helper.path2url(os.getcwd()) + '/'
fullhref = urllib.parse.urljoin(parentHref, self.href)
# all possible exceptions are ignored
try:
usedEncoding, enctype, cssText = self.parentStyleSheet._resolveImport(
fullhref
)
if cssText is None:
# catched in next except below!
raise OSError('Cannot read Stylesheet.')
# contentEncoding with parentStyleSheet.overrideEncoding,
# HTTP or parent
encodingOverride, encoding = None, None
if enctype == 0:
encodingOverride = usedEncoding
elif 0 < enctype < 5:
encoding = usedEncoding
# inherit fetcher for @imports in styleSheet
importedSheet._href = fullhref
importedSheet._setFetcher(self.parentStyleSheet._fetcher)
importedSheet._setCssTextWithEncodingOverride(
cssText, encodingOverride=encodingOverride, encoding=encoding
)
except (OSError, ValueError) as e:
self._log.warn(
'CSSImportRule: While processing imported '
'style sheet href=%s: %r' % (self.href, e),
neverraise=True,
)
else:
# used by resolveImports if to keep unprocessed href
self.hrefFound = True
self._styleSheet = importedSheet
_href = None # needs to be set
href = property(
lambda self: self._href,
_setHref,
doc="Location of the style sheet to be imported.",
)
def _setMedia(self, media):
"""
:param media:
a :class:`~cssutils.stylesheets.MediaList` or string
"""
self._checkReadonly()
if isinstance(media, str):
self._media = cssutils.stylesheets.MediaList(
mediaText=media, parentRule=self
)
else:
media._parentRule = self
self._media = media
# update seq
ihref = 0
for i, item in enumerate(self.seq):
if item.type == 'href':
ihref = i
elif item.type == 'media':
self.seq[i] = (self._media, 'media', None, None)
break
else:
# if no media until now add after href
self.seq.insert(ihref + 1, self._media, 'media', None, None)
media = property(
lambda self: self._media,
_setMedia,
doc="(DOM) A list of media types for this rule "
"of type :class:`~cssutils.stylesheets.MediaList`.",
)
def _setName(self, name=''):
"""Raises xml.dom.SyntaxErr if name is not a string."""
if name is None or isinstance(name, str):
# "" or '' handled as None
if not name:
name = None
# save name
self._name = name
# update seq
for i, item in enumerate(self.seq):
typ = item.type
if 'name' == typ:
self._seq[i] = (name, typ, item.line, item.col)
break
# set title of imported sheet
if self.styleSheet:
self.styleSheet.title = name
else:
self._log.error('CSSImportRule: Not a valid name: %s' % name)
def _getName(self):
return self._name
name = property(
_getName,
_setName,
doc="An optional name for the imported sheet.",
)
styleSheet = property(
lambda self: self._styleSheet,
doc="(readonly) The style sheet referred to by this " "rule.",
)
type = property(
lambda self: self.IMPORT_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
def _getWellformed(self):
"Depending on if media is used at all."
if self._usemedia:
return bool(self.href and self.media.wellformed)
else:
return bool(self.href)
wellformed = property(_getWellformed)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssmediarule.py 0000644 0001751 0000177 00000027252 14627633762 020575 0 ustar 00runner docker """CSSMediaRule implements DOM Level 2 CSS CSSMediaRule."""
__all__ = ['CSSMediaRule']
import xml.dom
import cssutils
from . import cssrule
class CSSMediaRule(cssrule.CSSRuleRules):
"""
Objects implementing the CSSMediaRule interface can be identified by the
MEDIA_RULE constant. On these objects the type attribute must return the
value of that constant.
Format::
: MEDIA_SYM S* medium [ COMMA S* medium ]*
STRING? # the name
LBRACE S* ruleset* '}' S*;
``cssRules``
All Rules in this media rule, a :class:`~cssutils.css.CSSRuleList`.
"""
def __init__(
self,
mediaText='all',
name=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""constructor"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@media'
# 1. media
if mediaText:
self.media = mediaText
else:
self.media = cssutils.stylesheets.MediaList()
self.name = name
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(mediaText={self.media.mediaText!r})"
def __str__(self):
return f""
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSMediaRule(self)
def _setCssText(self, cssText): # noqa: C901
"""
:param cssText:
a parseable string or a tuple of (cssText, dict-of-namespaces)
:Exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if a specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
# media "name"? { cssRules }
super()._setCssText(cssText)
# might be (cssText, namespaces)
cssText, namespaces = self._splitNamespacesOff(cssText)
tokenizer = self._tokenize2(cssText)
attoken = self._nexttoken(tokenizer, None)
if self._type(attoken) != self._prods.MEDIA_SYM:
self._log.error(
'CSSMediaRule: No CSSMediaRule found: %s' % self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
# save if parse goes wrong
oldMedia = self._media
oldCssRules = self._cssRules
ok = True
# media
mediatokens, end = self._tokensupto2(
tokenizer, mediaqueryendonly=True, separateEnd=True
)
if '{' == self._tokenvalue(end) or self._prods.STRING == self._type(end):
self.media = cssutils.stylesheets.MediaList(parentRule=self)
# TODO: remove special case
self.media.mediaText = mediatokens
ok = ok and self.media.wellformed
else:
ok = False
# name (optional)
name = None
nameseq = self._tempSeq()
if self._prods.STRING == self._type(end):
name = self._stringtokenvalue(end)
# TODO: for now comments are lost after name
nametokens, end = self._tokensupto2(
tokenizer, blockstartonly=True, separateEnd=True
)
wellformed, expected = self._parse(None, nameseq, nametokens, {})
if not wellformed:
ok = False
self._log.error(
'CSSMediaRule: Syntax Error: %s' % self._valuestr(cssText)
)
# check for {
if '{' != self._tokenvalue(end):
self._log.error(
'CSSMediaRule: No "{" found: %s' % self._valuestr(cssText)
)
return
# cssRules
cssrulestokens, braceOrEOF = self._tokensupto2(
tokenizer, mediaendonly=True, separateEnd=True
)
nonetoken = self._nexttoken(tokenizer, None)
if 'EOF' == self._type(braceOrEOF):
# HACK!!!
# TODO: Not complete, add EOF to rule and } to @media
cssrulestokens.append(braceOrEOF)
braceOrEOF = ('CHAR', '}', 0, 0)
self._log.debug(
'CSSMediaRule: Incomplete, adding "}".',
token=braceOrEOF,
neverraise=True,
)
if '}' != self._tokenvalue(braceOrEOF):
self._log.error('CSSMediaRule: No "}" found.', token=braceOrEOF)
elif nonetoken:
self._log.error(
'CSSMediaRule: Trailing content found.', token=nonetoken
)
else:
# for closures: must be a mutable
new = {'wellformed': True}
def COMMENT(expected, seq, token, tokenizer=None):
self.insertRule(
cssutils.css.CSSComment(
[token],
parentRule=self,
parentStyleSheet=self.parentStyleSheet,
)
)
return expected
def ruleset(expected, seq, token, tokenizer):
rule = cssutils.css.CSSStyleRule(
parentRule=self, parentStyleSheet=self.parentStyleSheet
)
rule.cssText = self._tokensupto2(tokenizer, token)
if rule.wellformed:
self.insertRule(rule)
return expected
def atrule(expected, seq, token, tokenizer):
# TODO: get complete rule!
tokens = self._tokensupto2(tokenizer, token)
atval = self._tokenvalue(token)
factories = {
'@page': cssutils.css.CSSPageRule,
'@media': CSSMediaRule,
}
if atval in (
'@charset ',
'@font-face',
'@import',
'@namespace',
'@variables',
):
self._log.error(
'CSSMediaRule: This rule is not '
'allowed in CSSMediaRule - ignored: '
'%s.' % self._valuestr(tokens),
token=token,
error=xml.dom.HierarchyRequestErr,
)
elif atval in factories:
rule = factories[atval](
parentRule=self, parentStyleSheet=self.parentStyleSheet
)
rule.cssText = tokens
if rule.wellformed:
self.insertRule(rule)
else:
rule = cssutils.css.CSSUnknownRule(
tokens,
parentRule=self,
parentStyleSheet=self.parentStyleSheet,
)
if rule.wellformed:
self.insertRule(rule)
return expected
# save for possible reset
oldCssRules = self.cssRules
self.cssRules = cssutils.css.CSSRuleList()
seq = [] # not used really
tokenizer = iter(cssrulestokens)
wellformed, expected = self._parse(
braceOrEOF,
seq,
tokenizer,
{
'COMMENT': COMMENT,
'CHARSET_SYM': atrule,
'FONT_FACE_SYM': atrule,
'IMPORT_SYM': atrule,
'NAMESPACE_SYM': atrule,
'PAGE_SYM': atrule,
'MEDIA_SYM': atrule,
'ATKEYWORD': atrule,
},
default=ruleset,
new=new,
)
ok = ok and wellformed
if ok:
self.name = name
self._setSeq(nameseq)
else:
self._media = oldMedia
self._cssRules = oldCssRules
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) The parsable textual representation of this " "rule.",
)
@property
def name(self):
"""An optional name for this media rule."""
return self._name
@name.setter
def name(self, name):
if isinstance(name, str) or name is None:
# "" or ''
if not name:
name = None
self._name = name
else:
self._log.error('CSSImportRule: Not a valid name: %s' % name)
def _setName(self, name):
self.name = name
def _setMedia(self, media):
"""
:param media:
a :class:`~cssutils.stylesheets.MediaList` or string
"""
self._checkReadonly()
if isinstance(media, str):
self._media = cssutils.stylesheets.MediaList(
mediaText=media, parentRule=self
)
else:
media._parentRule = self
self._media = media
# NOT IN @media seq at all?!
# # update seq
# for i, item in enumerate(self.seq):
# if item.type == 'media':
# self._seq[i] = (self._media, 'media', None, None)
# break
# else:
# # insert after @media if not in seq at all
# self.seq.insert(0,
# self._media, 'media', None, None)
media = property(
lambda self: self._media,
_setMedia,
doc="(DOM) A list of media types for this rule "
"of type :class:`~cssutils.stylesheets.MediaList`.",
)
def insertRule(self, rule, index=None):
"""Implements base ``insertRule``."""
rule, index = self._prepareInsertRule(rule, index)
if rule is False or rule is True:
# done or error
return
# check hierarchy
if (
isinstance(rule, cssutils.css.CSSCharsetRule)
or isinstance(rule, cssutils.css.CSSFontFaceRule)
or isinstance(rule, cssutils.css.CSSImportRule)
or isinstance(rule, cssutils.css.CSSNamespaceRule)
or isinstance(rule, cssutils.css.MarginRule)
):
self._log.error(
'%s: This type of rule is not allowed here: %s'
% (self.__class__.__name__, rule.cssText),
error=xml.dom.HierarchyRequestErr,
)
return
return self._finishInsertRule(rule, index)
type = property(
lambda self: self.MEDIA_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
wellformed = property(lambda self: self.media.wellformed)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssnamespacerule.py 0000644 0001751 0000177 00000025604 14627633762 021451 0 ustar 00runner docker """CSSNamespaceRule currently implements http://dev.w3.org/csswg/css3-namespace/"""
__all__ = ['CSSNamespaceRule']
import xml.dom
import cssutils
from . import cssrule
class CSSNamespaceRule(cssrule.CSSRule):
"""
Represents an @namespace rule within a CSS style sheet.
The @namespace at-rule declares a namespace prefix and associates
it with a given namespace (a string). This namespace prefix can then be
used in namespace-qualified names such as those described in the
Selectors Module [SELECT] or the Values and Units module [CSS3VAL].
Dealing with these rules directly is not needed anymore, easier is
the use of :attr:`cssutils.css.CSSStyleSheet.namespaces`.
Format::
namespace
: NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
;
namespace_prefix
: IDENT
;
"""
def __init__(
self,
namespaceURI=None,
prefix=None,
cssText=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""
:Parameters:
namespaceURI
The namespace URI (a simple string!) which is bound to the
given prefix. If no prefix is set
(``CSSNamespaceRule.prefix==''``) the namespace defined by
namespaceURI is set as the default namespace
prefix
The prefix used in the stylesheet for the given
``CSSNamespaceRule.uri``.
cssText
if no namespaceURI is given cssText must be given to set
a namespaceURI as this is readonly later on
parentStyleSheet
sheet where this rule belongs to
Do not use as positional but as keyword parameters only!
If readonly allows setting of properties in constructor only
format namespace::
namespace
: NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
;
namespace_prefix
: IDENT
;
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@namespace'
self._prefix = ''
self._namespaceURI = None
if namespaceURI:
self.namespaceURI = namespaceURI
self.prefix = prefix
tempseq = self._tempSeq()
tempseq.append(self.prefix, 'prefix')
tempseq.append(self.namespaceURI, 'namespaceURI')
self._setSeq(tempseq)
elif cssText is not None:
self.cssText = cssText
if parentStyleSheet:
self._parentStyleSheet = parentStyleSheet
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(namespaceURI={self.namespaceURI!r}, prefix={self.prefix!r})"
def __str__(self):
return f""
def _getCssText(self):
"""Return serialized property cssText"""
return cssutils.ser.do_CSSNamespaceRule(self)
def _setCssText(self, cssText): # noqa: C901
"""
:param cssText: initial value for this rules cssText which is parsed
:exceptions:
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
attoken = self._nexttoken(tokenizer, None)
if self._type(attoken) != self._prods.NAMESPACE_SYM:
self._log.error(
'CSSNamespaceRule: No CSSNamespaceRule found: %s'
% self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
# for closures: must be a mutable
new = {
'keyword': self._tokenvalue(attoken),
'prefix': '',
'uri': None,
'wellformed': True,
}
def _ident(expected, seq, token, tokenizer=None):
# the namespace prefix, optional
if 'prefix or uri' == expected:
new['prefix'] = self._tokenvalue(token)
seq.append(new['prefix'], 'prefix')
return 'uri'
else:
new['wellformed'] = False
self._log.error('CSSNamespaceRule: Unexpected ident.', token)
return expected
def _string(expected, seq, token, tokenizer=None):
# the namespace URI as a STRING
if expected.endswith('uri'):
new['uri'] = self._stringtokenvalue(token)
seq.append(new['uri'], 'namespaceURI')
return ';'
else:
new['wellformed'] = False
self._log.error('CSSNamespaceRule: Unexpected string.', token)
return expected
def _uri(expected, seq, token, tokenizer=None):
# the namespace URI as URI which is DEPRECATED
if expected.endswith('uri'):
uri = self._uritokenvalue(token)
new['uri'] = uri
seq.append(new['uri'], 'namespaceURI')
return ';'
else:
new['wellformed'] = False
self._log.error('CSSNamespaceRule: Unexpected URI.', token)
return expected
def _char(expected, seq, token, tokenizer=None):
# final ;
val = self._tokenvalue(token)
if ';' == expected and ';' == val:
return 'EOF'
else:
new['wellformed'] = False
self._log.error('CSSNamespaceRule: Unexpected char.', token)
return expected
# "NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*"
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected='prefix or uri',
seq=newseq,
tokenizer=tokenizer,
productions={
'IDENT': _ident,
'STRING': _string,
'URI': _uri,
'CHAR': _char,
},
new=new,
)
# wellformed set by parse
wellformed = wellformed and new['wellformed']
# post conditions
if new['uri'] is None:
wellformed = False
self._log.error(
'CSSNamespaceRule: No namespace URI found: %s'
% self._valuestr(cssText)
)
if expected != 'EOF':
wellformed = False
self._log.error(
'CSSNamespaceRule: No ";" found: %s' % self._valuestr(cssText)
)
# set all
if wellformed:
self.atkeyword = new['keyword']
self._prefix = new['prefix']
self.namespaceURI = new['uri']
self._setSeq(newseq)
cssText = property(
fget=_getCssText,
fset=_setCssText,
doc="(DOM) The parsable textual representation of this " "rule.",
)
def _setNamespaceURI(self, namespaceURI):
"""
:param namespaceURI: the initial value for this rules namespaceURI
:exceptions:
- :exc:`~xml.dom.NoModificationAllowedErr`:
(CSSRule) Raised if this rule is readonly or a namespaceURI is
already set in this rule.
"""
self._checkReadonly()
if not self._namespaceURI:
# initial setting
self._namespaceURI = namespaceURI
tempseq = self._tempSeq()
tempseq.append(namespaceURI, 'namespaceURI')
self._setSeq(tempseq) # makes seq readonly!
elif self._namespaceURI != namespaceURI:
self._log.error(
'CSSNamespaceRule: namespaceURI is readonly.',
error=xml.dom.NoModificationAllowedErr,
)
namespaceURI = property(
lambda self: self._namespaceURI,
_setNamespaceURI,
doc="URI (handled as simple string) of the defined namespace.",
)
def _replaceNamespaceURI(self, namespaceURI):
"""Used during parse of new sheet only!
:param namespaceURI: the new value for this rules namespaceURI
"""
self._namespaceURI = namespaceURI
for i, x in enumerate(self._seq):
if 'namespaceURI' == x.type:
self._seq._readonly = False
self._seq.replace(i, namespaceURI, 'namespaceURI')
self._seq._readonly = True
break
def _setPrefix(self, prefix=None):
"""
:param prefix: the new prefix
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
if not prefix:
prefix = ''
else:
tokenizer = self._tokenize2(prefix)
prefixtoken = self._nexttoken(tokenizer, None)
if not prefixtoken or self._type(prefixtoken) != self._prods.IDENT:
self._log.error(
'CSSNamespaceRule: No valid prefix "%s".' % self._valuestr(prefix),
error=xml.dom.SyntaxErr,
)
return
else:
prefix = self._tokenvalue(prefixtoken)
# update seq
for i, x in enumerate(self._seq):
if x == self._prefix:
self._seq[i] = (prefix, 'prefix', None, None)
break
else:
# put prefix at the beginning!
self._seq[0] = (prefix, 'prefix', None, None)
# set new prefix
self._prefix = prefix
prefix = property(
lambda self: self._prefix,
_setPrefix,
doc="Prefix used for the defined namespace.",
)
type = property(
lambda self: self.NAMESPACE_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
wellformed = property(lambda self: self.namespaceURI is not None)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/csspagerule.py 0000644 0001751 0000177 00000036113 14627633762 020426 0 ustar 00runner docker """CSSPageRule implements DOM Level 2 CSS CSSPageRule."""
__all__ = ['CSSPageRule']
import xml.dom
from itertools import chain
import cssutils
from . import cssrule
from .cssstyledeclaration import CSSStyleDeclaration
from .marginrule import MarginRule
class CSSPageRule(cssrule.CSSRuleRules):
"""
The CSSPageRule interface represents a @page rule within a CSS style
sheet. The @page rule is used to specify the dimensions, orientation,
margins, etc. of a page box for paged media.
Format::
page :
PAGE_SYM S* IDENT? pseudo_page? S*
'{' S* [ declaration | margin ]?
[ ';' S* [ declaration | margin ]? ]* '}' S*
;
pseudo_page :
':' [ "left" | "right" | "first" ]
;
margin :
margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
;
margin_sym :
TOPLEFTCORNER_SYM |
TOPLEFT_SYM |
TOPCENTER_SYM |
TOPRIGHT_SYM |
TOPRIGHTCORNER_SYM |
BOTTOMLEFTCORNER_SYM |
BOTTOMLEFT_SYM |
BOTTOMCENTER_SYM |
BOTTOMRIGHT_SYM |
BOTTOMRIGHTCORNER_SYM |
LEFTTOP_SYM |
LEFTMIDDLE_SYM |
LEFTBOTTOM_SYM |
RIGHTTOP_SYM |
RIGHTMIDDLE_SYM |
RIGHTBOTTOM_SYM
;
`cssRules` contains a list of `MarginRule` objects.
"""
def __init__(
self,
selectorText=None,
style=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""
If readonly allows setting of properties in constructor only.
:param selectorText:
type string
:param style:
CSSStyleDeclaration for this CSSStyleRule
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@page'
self._specificity = (0, 0, 0)
tempseq = self._tempSeq()
if selectorText:
self.selectorText = selectorText
tempseq.append(self.selectorText, 'selectorText')
else:
self._selectorText = self._tempSeq()
if style:
self.style = style
else:
self.style = CSSStyleDeclaration()
tempseq.append(self.style, 'style')
self._setSeq(tempseq)
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(selectorText={self.selectorText!r}, style={self.style.cssText!r})"
def __str__(self):
return (
""
) % (
self.__class__.__name__,
self.selectorText,
self.specificity,
self.style.cssText,
len(self.cssRules),
id(self),
)
def __contains__(self, margin):
"""Check if margin is set in the rule."""
return margin in list(self.keys())
def keys(self):
"Return list of all set margins (MarginRule)."
return list(r.margin for r in self.cssRules)
def __getitem__(self, margin):
"""Retrieve the style (of MarginRule)
for `margin` (which must be normalized).
"""
for r in self.cssRules:
if r.margin == margin:
return r.style
def __setitem__(self, margin, style):
"""Set the style (of MarginRule)
for `margin` (which must be normalized).
"""
for i, r in enumerate(self.cssRules):
if r.margin == margin:
r.style = style
return i
else:
return self.add(MarginRule(margin, style))
def __delitem__(self, margin):
"""Delete the style (the MarginRule)
for `margin` (which must be normalized).
"""
for r in self.cssRules:
if r.margin == margin:
self.deleteRule(r)
def __parseSelectorText(self, selectorText): # noqa: C901
"""
Parse `selectorText` which may also be a list of tokens
and returns (selectorText, seq).
see _setSelectorText for details
"""
# for closures: must be a mutable
new = {'wellformed': True, 'last-S': False, 'name': 0, 'first': 0, 'lr': 0}
def _char(expected, seq, token, tokenizer=None):
# pseudo_page, :left, :right or :first
val = self._tokenvalue(token)
if not new['last-S'] and expected in ['page', ': or EOF'] and ':' == val:
try:
identtoken = next(tokenizer)
except StopIteration:
self._log.error('CSSPageRule selectorText: No IDENT found.', token)
else:
ival, ityp = self._tokenvalue(identtoken), self._type(identtoken)
if self._prods.IDENT != ityp:
self._log.error(
'CSSPageRule selectorText: Expected '
'IDENT but found: %r' % ival,
token,
)
else:
if ival not in ('first', 'left', 'right'):
self._log.warn(
'CSSPageRule: Unknown @page '
'selector: %r' % (':' + ival,),
neverraise=True,
)
if ival == 'first':
new['first'] = 1
else:
new['lr'] = 1
seq.append(val + ival, 'pseudo')
return 'EOF'
return expected
else:
new['wellformed'] = False
self._log.error(
'CSSPageRule selectorText: Unexpected CHAR: %r' % val, token
)
return expected
def S(expected, seq, token, tokenizer=None):
"Does not raise if EOF is found."
if expected == ': or EOF':
# pseudo must directly follow IDENT if given
new['last-S'] = True
return expected
def IDENT(expected, seq, token, tokenizer=None):
""" """
val = self._tokenvalue(token)
if 'page' == expected:
if self._normalize(val) == 'auto':
self._log.error(
'CSSPageRule selectorText: Invalid pagename.', token
)
else:
new['name'] = 1
seq.append(val, 'IDENT')
return ': or EOF'
else:
new['wellformed'] = False
self._log.error(
'CSSPageRule selectorText: Unexpected IDENT: ' '%r' % val, token
)
return expected
def COMMENT(expected, seq, token, tokenizer=None):
"Does not raise if EOF is found."
seq.append(cssutils.css.CSSComment([token]), 'COMMENT')
return expected
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected='page',
seq=newseq,
tokenizer=self._tokenize2(selectorText),
productions={'CHAR': _char, 'IDENT': IDENT, 'COMMENT': COMMENT, 'S': S},
new=new,
)
wellformed = wellformed and new['wellformed']
# post conditions
if expected == 'ident':
self._log.error(
'CSSPageRule selectorText: No valid selector: %r'
% self._valuestr(selectorText)
)
return wellformed, newseq, (new['name'], new['first'], new['lr'])
def __parseMarginAndStyle(self, tokens):
"tokens is a list, no generator (yet)"
g = iter(tokens)
styletokens = []
# new rules until parse done
cssRules = []
for token in g:
if (
token[0] == 'ATKEYWORD'
and self._normalize(token[1]) in MarginRule.margins
):
# MarginRule
m = MarginRule(parentRule=self, parentStyleSheet=self.parentStyleSheet)
m.cssText = chain([token], g)
# merge if margin set more than once
for r in cssRules:
if r.margin == m.margin:
for p in m.style:
r.style.setProperty(p, replace=False)
break
else:
cssRules.append(m)
continue
# TODO: Properties?
styletokens.append(token)
return cssRules, styletokens
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSPageRule(self)
def _setCssText(self, cssText):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
if self._type(self._nexttoken(tokenizer)) != self._prods.PAGE_SYM:
self._log.error(
'CSSPageRule: No CSSPageRule found: %s' % self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
newStyle = CSSStyleDeclaration(parentRule=self)
ok = True
selectortokens, startbrace = self._tokensupto2(
tokenizer, blockstartonly=True, separateEnd=True
)
styletokens, braceorEOFtoken = self._tokensupto2(
tokenizer, blockendonly=True, separateEnd=True
)
nonetoken = self._nexttoken(tokenizer)
if self._tokenvalue(startbrace) != '{':
ok = False
self._log.error(
'CSSPageRule: No start { of style declaration '
'found: %r' % self._valuestr(cssText),
startbrace,
)
elif nonetoken:
ok = False
self._log.error('CSSPageRule: Trailing content found.', token=nonetoken)
selok, newselseq, specificity = self.__parseSelectorText(selectortokens)
ok = ok and selok
val, type_ = self._tokenvalue(braceorEOFtoken), self._type(braceorEOFtoken)
if val != '}' and type_ != 'EOF':
ok = False
self._log.error(
'CSSPageRule: No "}" after style declaration found: %r'
% self._valuestr(cssText)
)
else:
if 'EOF' == type_:
# add again as style needs it
styletokens.append(braceorEOFtoken)
# filter pagemargin rules out first
cssRules, styletokens = self.__parseMarginAndStyle(styletokens)
# SET, may raise:
newStyle.cssText = styletokens
if ok:
self._selectorText = newselseq
self._specificity = specificity
self.style = newStyle
self.cssRules = cssutils.css.CSSRuleList()
for r in cssRules:
self.cssRules.append(r)
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) The parsable textual representation of this rule.",
)
def _getSelectorText(self):
"""Wrapper for cssutils Selector object."""
return cssutils.ser.do_CSSPageRuleSelector(self._selectorText)
def _setSelectorText(self, selectorText):
"""Wrapper for cssutils Selector object.
:param selectorText:
DOM String, in CSS 2.1 one of
- :first
- :left
- :right
- empty
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
and is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
# may raise SYNTAX_ERR
wellformed, newseq, specificity = self.__parseSelectorText(selectorText)
if wellformed:
self._selectorText = newseq
self._specificity = specificity
selectorText = property(
_getSelectorText,
_setSelectorText,
doc="(DOM) The parsable textual representation of "
"the page selector for the rule.",
)
def _setStyle(self, style):
"""
:param style:
a CSSStyleDeclaration or string
"""
self._checkReadonly()
if isinstance(style, str):
self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
else:
style._parentRule = self
self._style = style
style = property(
lambda self: self._style,
_setStyle,
doc="(DOM) The declaration-block of this rule set, "
"a :class:`~cssutils.css.CSSStyleDeclaration`.",
)
def insertRule(self, rule, index=None):
"""Implements base ``insertRule``."""
rule, index = self._prepareInsertRule(rule, index)
if rule is False or rule is True:
# done or error
return
# check hierarchy
if (
isinstance(rule, cssutils.css.CSSCharsetRule)
or isinstance(rule, cssutils.css.CSSFontFaceRule)
or isinstance(rule, cssutils.css.CSSImportRule)
or isinstance(rule, cssutils.css.CSSNamespaceRule)
or isinstance(rule, CSSPageRule)
or isinstance(rule, cssutils.css.CSSMediaRule)
):
self._log.error(
'%s: This type of rule is not allowed here: %s'
% (self.__class__.__name__, rule.cssText),
error=xml.dom.HierarchyRequestErr,
)
return
return self._finishInsertRule(rule, index)
specificity = property(
lambda self: self._specificity,
doc="""Specificity of this page rule (READONLY).
Tuple of (f, g, h) where:
- if the page selector has a named page, f=1; else f=0
- if the page selector has a ':first' pseudo-class, g=1; else g=0
- if the page selector has a ':left' or ':right' pseudo-class, h=1; else h=0
""",
)
type = property(
lambda self: self.PAGE_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
# constant but needed:
wellformed = property(lambda self: True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssproperties.py 0000644 0001751 0000177 00000012110 14627633762 021005 0 ustar 00runner docker """CSS2Properties (partly!) implements DOM Level 2 CSS CSS2Properties used
by CSSStyleDeclaration
TODO: CSS2Properties
If an implementation does implement this interface, it is expected to
understand the specific syntax of the shorthand properties, and apply
their semantics; when the margin property is set, for example, the
marginTop, marginRight, marginBottom and marginLeft properties are
actually being set by the underlying implementation.
When dealing with CSS "shorthand" properties, the shorthand properties
should be decomposed into their component longhand properties as
appropriate, and when querying for their value, the form returned
should be the shortest form exactly equivalent to the declarations made
in the ruleset. However, if there is no shorthand declaration that
could be added to the ruleset without changing in any way the rules
already declared in the ruleset (i.e., by adding longhand rules that
were previously not declared in the ruleset), then the empty string
should be returned for the shorthand property.
For example, querying for the font property should not return
"normal normal normal 14pt/normal Arial, sans-serif", when
"14pt Arial, sans-serif" suffices. (The normals are initial values, and
are implied by use of the longhand property.)
If the values for all the longhand properties that compose a particular
string are the initial values, then a string consisting of all the
initial values should be returned (e.g. a border-width value of
"medium" should be returned as such, not as "").
For some shorthand properties that take missing values from other
sides, such as the margin, padding, and border-[width|style|color]
properties, the minimum number of sides possible should be used; i.e.,
"0px 10px" will be returned instead of "0px 10px 0px 10px".
If the value of a shorthand property can not be decomposed into its
component longhand properties, as is the case for the font property
with a value of "menu", querying for the values of the component
longhand properties should return the empty string.
TODO: CSS2Properties DOMImplementation
The interface found within this section are not mandatory. A DOM
application can use the hasFeature method of the DOMImplementation
interface to determine whether it is supported or not. The feature
string for this extended interface listed in this section is "CSS2"
and the version is "2.0".
"""
__all__ = ['CSS2Properties']
import re
import cssutils.profiles
class CSS2Properties:
"""The CSS2Properties interface represents a convenience mechanism
for retrieving and setting properties within a CSSStyleDeclaration.
The attributes of this interface correspond to all the properties
specified in CSS2. Getting an attribute of this interface is
equivalent to calling the getPropertyValue method of the
CSSStyleDeclaration interface. Setting an attribute of this
interface is equivalent to calling the setProperty method of the
CSSStyleDeclaration interface.
cssutils actually also allows usage of ``del`` to remove a CSS property
from a CSSStyleDeclaration.
This is an abstract class, the following functions need to be present
in inheriting class:
- ``_getP``
- ``_setP``
- ``_delP``
"""
# actual properties are set after the class definition!
def _getP(self, CSSname):
pass
def _setP(self, CSSname, value):
pass
def _delP(self, CSSname):
pass
_reCSStoDOMname = re.compile('-[a-z]', re.I)
def _toDOMname(CSSname):
"""Returns DOMname for given CSSname e.g. for CSSname 'font-style' returns
'fontStyle'.
"""
def _doCSStoDOMname2(m):
return m.group(0)[1].capitalize()
return _reCSStoDOMname.sub(_doCSStoDOMname2, CSSname)
_reDOMtoCSSname = re.compile('([A-Z])[a-z]+')
def _toCSSname(DOMname):
"""Return CSSname for given DOMname e.g. for DOMname 'fontStyle' returns
'font-style'.
"""
def _doDOMtoCSSname2(m):
return '-' + m.group(0).lower()
return _reDOMtoCSSname.sub(_doDOMtoCSSname2, DOMname)
# add list of DOMname properties to CSS2Properties
# used for CSSStyleDeclaration to check if allowed properties
# but somehow doubled, any better way?
CSS2Properties._properties = []
for group in cssutils.profiles.properties:
for name in cssutils.profiles.properties[group]:
CSS2Properties._properties.append(_toDOMname(name))
# add CSS2Properties to CSSStyleDeclaration:
def __named_property_def(DOMname):
"""
Closure to keep name known in each properties accessor function
DOMname is converted to CSSname here, so actual calls use CSSname.
"""
CSSname = _toCSSname(DOMname)
def _get(self):
return self._getP(CSSname)
def _set(self, value):
self._setP(CSSname, value)
def _del(self):
self._delP(CSSname)
return _get, _set, _del
# add all CSS2Properties to CSSStyleDeclaration
for DOMname in CSS2Properties._properties:
setattr(CSS2Properties, DOMname, property(*__named_property_def(DOMname)))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssrule.py 0000644 0001751 0000177 00000026045 14627633762 017574 0 ustar 00runner docker """CSSRule implements DOM Level 2 CSS CSSRule."""
__all__ = ['CSSRule']
import xml.dom
import cssutils.util
class CSSRule(cssutils.util.Base2):
"""Abstract base interface for any type of CSS statement. This includes
both rule sets and at-rules. An implementation is expected to preserve
all rules specified in a CSS style sheet, even if the rule is not
recognized by the parser. Unrecognized rules are represented using the
``CSSUnknownRule`` interface.
"""
"""
CSSRule type constants.
An integer indicating which type of rule this is.
"""
UNKNOWN_RULE = 0
"``cssutils.css.CSSUnknownRule`` (not used in CSSOM anymore)"
STYLE_RULE = 1
"``cssutils.css.CSSStyleRule``"
CHARSET_RULE = 2
"``cssutils.css.CSSCharsetRule`` (not used in CSSOM anymore)"
IMPORT_RULE = 3
"``cssutils.css.CSSImportRule``"
MEDIA_RULE = 4
"``cssutils.css.CSSMediaRule``"
FONT_FACE_RULE = 5
"``cssutils.css.CSSFontFaceRule``"
PAGE_RULE = 6
"``cssutils.css.CSSPageRule``"
NAMESPACE_RULE = 10
"""``cssutils.css.CSSNamespaceRule``,
Value has changed in 0.9.7a3 due to a change in the CSSOM spec."""
COMMENT = 1001 # was -1, cssutils only
"""``cssutils.css.CSSComment`` - not in the offical spec,
Value has changed in 0.9.7a3"""
VARIABLES_RULE = 1008
"""``cssutils.css.CSSVariablesRule`` - experimental rule
not in the offical spec"""
MARGIN_RULE = 1006
"""``cssutils.css.MarginRule`` - experimental rule
not in the offical spec"""
_typestrings = {
UNKNOWN_RULE: 'UNKNOWN_RULE',
STYLE_RULE: 'STYLE_RULE',
CHARSET_RULE: 'CHARSET_RULE',
IMPORT_RULE: 'IMPORT_RULE',
MEDIA_RULE: 'MEDIA_RULE',
FONT_FACE_RULE: 'FONT_FACE_RULE',
PAGE_RULE: 'PAGE_RULE',
NAMESPACE_RULE: 'NAMESPACE_RULE',
COMMENT: 'COMMENT',
VARIABLES_RULE: 'VARIABLES_RULE',
MARGIN_RULE: 'MARGIN_RULE',
}
def __init__(self, parentRule=None, parentStyleSheet=None, readonly=False):
"""Set common attributes for all rules."""
super().__init__()
self._parent = parentRule
self._parentRule = parentRule
self._parentStyleSheet = parentStyleSheet
self._setSeq(self._tempSeq())
# self._atkeyword = None
# must be set after initialization of #inheriting rule is done
self._readonly = False
def _setAtkeyword(self, keyword):
"""Check if new keyword fits the rule it is used for."""
atkeyword = self._normalize(keyword)
if not self.atkeyword or (self.atkeyword == atkeyword):
self._atkeyword = atkeyword
self._keyword = keyword
else:
self._log.error(
f'{self.atkeyword}: Invalid atkeyword for this rule: {keyword!r}',
error=xml.dom.InvalidModificationErr,
)
atkeyword = property(
lambda self: self._atkeyword,
_setAtkeyword,
doc="Normalized keyword of an @rule (e.g. ``@import``).",
)
def _setCssText(self, cssText):
"""
:param cssText:
A parsable DOMString.
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
self._checkReadonly()
cssText = property(
lambda self: '',
_setCssText,
doc="(DOM) The parsable textual representation of the "
"rule. This reflects the current state of the rule "
"and not its initial value.",
)
@property
def parent(self):
"""The Parent Node of this CSSRule or None."""
return self._parent
parentRule = property(
lambda self: self._parentRule,
doc="If this rule is contained inside another rule "
"(e.g. a style rule inside an @media block), this "
"is the containing rule. If this rule is not nested "
"inside any other rules, this returns None.",
)
def _getParentStyleSheet(self):
# rules contained in other rules (@media) use that rules parent
if self.parentRule:
return self.parentRule._parentStyleSheet
else:
return self._parentStyleSheet
parentStyleSheet = property(
_getParentStyleSheet, doc="The style sheet that contains this rule."
)
type = property(
lambda self: self.UNKNOWN_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
typeString = property(
lambda self: CSSRule._typestrings[self.type],
doc="Descriptive name of this rule's type.",
)
wellformed = property(lambda self: False, doc="If the rule is wellformed.")
class CSSRuleRules(CSSRule):
"""Abstract base interface for rules that contain other rules
like @media or @page. Methods may be overwritten if a rule
has specific stuff to do like checking the order of insertion like
@media does.
"""
def __init__(self, parentRule=None, parentStyleSheet=None):
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self.cssRules = cssutils.css.CSSRuleList()
def __iter__(self):
"""Generator iterating over these rule's cssRules."""
yield from self._cssRules
def _setCssRules(self, cssRules):
"Set new cssRules and update contained rules refs."
cssRules.append = self.insertRule
cssRules.extend = self.insertRule
cssRules.__delitem__ = self.deleteRule
for rule in cssRules:
rule._parentRule = self
rule._parentStyleSheet = None
self._cssRules = cssRules
cssRules = property(
lambda self: self._cssRules,
_setCssRules,
"All Rules in this style sheet, a " ":class:`~cssutils.css.CSSRuleList`.",
)
def deleteRule(self, index):
"""
Delete the rule at `index` from rules ``cssRules``.
:param index:
The `index` of the rule to be removed from the rules cssRules
list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is
raised but rules for normal Python lists are used. E.g.
``deleteRule(-1)`` removes the last rule in cssRules.
`index` may also be a CSSRule object which will then be removed.
:Exceptions:
- :exc:`~xml.dom.IndexSizeErr`:
Raised if the specified index does not correspond to a rule in
the media rule list.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this media rule is readonly.
"""
self._checkReadonly()
if isinstance(index, CSSRule):
for i, r in enumerate(self.cssRules):
if index == r:
index = i
break
else:
raise xml.dom.IndexSizeErr(
"%s: Not a rule in "
"this rule'a cssRules list: %s" % (self.__class__.__name__, index)
)
try:
# detach
self._cssRules[index]._parentRule = None
del self._cssRules[index]
except IndexError as err:
raise xml.dom.IndexSizeErr(
'%s: %s is not a valid index '
'in the rulelist of length %i'
% (self.__class__.__name__, index, self._cssRules.length)
) from err
def _prepareInsertRule(self, rule, index=None):
"return checked `index` and optional parsed `rule`"
self._checkReadonly()
# check index
if index is None:
index = len(self._cssRules)
elif index < 0 or index > self._cssRules.length:
raise xml.dom.IndexSizeErr(
'%s: Invalid index %s for '
'CSSRuleList with a length of %s.'
% (self.__class__.__name__, index, self._cssRules.length)
)
# check and optionally parse rule
if isinstance(rule, str):
tempsheet = cssutils.css.CSSStyleSheet()
tempsheet.cssText = rule
if len(tempsheet.cssRules) != 1 or (
tempsheet.cssRules
and not isinstance(tempsheet.cssRules[0], cssutils.css.CSSRule)
):
self._log.error(f'{self.__class__.__name__}: Invalid Rule: {rule}')
return False, False
rule = tempsheet.cssRules[0]
elif isinstance(rule, cssutils.css.CSSRuleList):
# insert all rules
for i, r in enumerate(rule):
self.insertRule(r, index + i)
return True, True
elif not isinstance(rule, cssutils.css.CSSRule):
self._log.error(f'{rule}: Not a CSSRule: {self.__class__.__name__}')
return False, False
return rule, index
def _finishInsertRule(self, rule, index):
"add `rule` at `index`"
rule._parentRule = self
rule._parentStyleSheet = None
self._cssRules.insert(index, rule)
return index
def add(self, rule):
"""Add `rule` to page rule. Same as ``insertRule(rule)``."""
return self.insertRule(rule)
def insertRule(self, rule, index=None):
"""
Insert `rule` into the rules ``cssRules``.
:param rule:
the parsable text representing the `rule` to be inserted. For rule
sets this contains both the selector and the style declaration.
For at-rules, this specifies both the at-identifier and the rule
content.
cssutils also allows rule to be a valid
:class:`~cssutils.css.CSSRule` object.
:param index:
before the `index` the specified `rule` will be inserted.
If the specified `index` is equal to the length of the rules
rule collection, the rule will be added to the end of the rule.
If index is not given or None rule will be appended to rule
list.
:returns:
the index of the newly inserted rule.
:exceptions:
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the `rule` cannot be inserted at the specified `index`,
e.g., if an @import rule is inserted after a standard rule set
or other at-rule.
- :exc:`~xml.dom.IndexSizeErr`:
Raised if the specified `index` is not a valid insertion point.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified `rule` has a syntax error and is
unparsable.
"""
return self._prepareInsertRule(rule, index)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssrulelist.py 0000644 0001751 0000177 00000003673 14627633762 020472 0 ustar 00runner docker """CSSRuleList implements DOM Level 2 CSS CSSRuleList.
Partly also http://dev.w3.org/csswg/cssom/#the-cssrulelist."""
__all__ = ['CSSRuleList']
class CSSRuleList(list):
"""The CSSRuleList object represents an (ordered) list of statements.
The items in the CSSRuleList are accessible via an integral index,
starting from 0.
Subclasses a standard Python list so theoretically all standard list
methods are available. Setting methods like ``__init__``, ``append``,
``extend`` or ``__setslice__`` are added later on instances of this
class if so desired.
E.g. CSSStyleSheet adds ``append`` which is not available in a simple
instance of this class!
"""
def __init__(self, *ignored):
"Nothing is set as this must also be defined later."
pass
def __notimplemented(self, *ignored):
"Implemented in class using a CSSRuleList only."
raise NotImplementedError(
'Must be implemented by class using an instance of this class.'
)
append = extend = __setitem__ = __setslice__ = __notimplemented
def item(self, index):
"""(DOM) Retrieve a CSS rule by ordinal `index`. The order in this
collection represents the order of the rules in the CSS style
sheet. If index is greater than or equal to the number of rules in
the list, this returns None.
Returns CSSRule, the style rule at the index position in the
CSSRuleList, or None if that is not a valid index.
"""
try:
return self[index]
except IndexError:
return None
@property
def length(self):
"""(DOM) The number of CSSRules in the list."""
return len(self)
def rulesOfType(self, type):
"""Yield the rules which have the given `type` only, one of the
constants defined in :class:`cssutils.css.CSSRule`."""
for r in self:
if r.type == type:
yield r
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssstyledeclaration.py 0000644 0001751 0000177 00000064777 14627633762 022211 0 ustar 00runner docker """CSSStyleDeclaration implements DOM Level 2 CSS CSSStyleDeclaration and
extends CSS2Properties
see
http://www.w3.org/TR/1998/REC-CSS2-19980512/syndata.html#parsing-errors
Unknown properties
------------------
User agents must ignore a declaration with an unknown property.
For example, if the style sheet is::
H1 { color: red; rotation: 70minutes }
the user agent will treat this as if the style sheet had been::
H1 { color: red }
Cssutils gives a message about any unknown properties but
keeps any property (if syntactically correct).
Illegal values
--------------
User agents must ignore a declaration with an illegal value. For example::
IMG { float: left } /* correct CSS2 */
IMG { float: left here } /* "here" is not a value of 'float' */
IMG { background: "red" } /* keywords cannot be quoted in CSS2 */
IMG { border-width: 3 } /* a unit must be specified for length values */
A CSS2 parser would honor the first rule and ignore the rest, as if the
style sheet had been::
IMG { float: left }
IMG { }
IMG { }
IMG { }
Cssutils again will issue a message (WARNING in this case) about invalid
CSS2 property values.
TODO:
This interface is also used to provide a read-only access to the
computed values of an element. See also the ViewCSS interface.
- return computed values and not literal values
- simplify unit pairs/triples/quadruples
2px 2px 2px 2px -> 2px for border/padding...
- normalize compound properties like:
background: no-repeat left url() #fff
-> background: #fff url() no-repeat left
"""
__all__ = ['CSSStyleDeclaration', 'Property']
import cssutils
from .cssproperties import CSS2Properties
from .property import Property
class CSSStyleDeclaration(CSS2Properties, cssutils.util.Base2):
"""The CSSStyleDeclaration class represents a single CSS declaration
block. This class may be used to determine the style properties
currently set in a block or to set style properties explicitly
within the block.
While an implementation may not recognize all CSS properties within
a CSS declaration block, it is expected to provide access to all
specified properties in the style sheet through the
CSSStyleDeclaration interface.
Furthermore, implementations that support a specific level of CSS
should correctly handle CSS shorthand properties for that level. For
a further discussion of shorthand properties, see the CSS2Properties
interface.
Additionally the CSS2Properties interface is implemented.
$css2propertyname
All properties defined in the CSS2Properties class are available
as direct properties of CSSStyleDeclaration with their respective
DOM name, so e.g. ``fontStyle`` for property 'font-style'.
These may be used as::
>>> style = CSSStyleDeclaration(cssText='color: red')
>>> style.color = 'green'
>>> print(style.color)
green
>>> del style.color
>>> print(style.color)
Format::
[Property: Value Priority?;]* [Property: Value Priority?]?
"""
def __init__(self, cssText='', parentRule=None, readonly=False, validating=None):
"""
:param cssText:
Shortcut, sets CSSStyleDeclaration.cssText
:param parentRule:
The CSS rule that contains this declaration block or
None if this CSSStyleDeclaration is not attached to a CSSRule.
:param readonly:
defaults to False
:param validating:
a flag defining if this sheet should be validated on change.
Defaults to None, which means defer to the parent stylesheet.
"""
super().__init__()
self._parentRule = parentRule
self.validating = validating
self.cssText = cssText
self._readonly = readonly
def __contains__(self, nameOrProperty):
"""Check if a property (or a property with given name) is in style.
:param name:
a string or Property, uses normalized name and not literalname
"""
if isinstance(nameOrProperty, Property):
name = nameOrProperty.name
else:
name = self._normalize(nameOrProperty)
return name in self.__nnames()
def __iter__(self):
"""Iterator of set Property objects with different normalized names."""
def properties():
for name in self.__nnames():
yield self.getProperty(name)
return properties()
def keys(self):
"""Analoguous to standard dict returns property names which are set in
this declaration."""
return list(self.__nnames())
def __getitem__(self, CSSName):
"""Retrieve the value of property ``CSSName`` from this declaration.
``CSSName`` will be always normalized.
"""
return self.getPropertyValue(CSSName)
def __setitem__(self, CSSName, value):
"""Set value of property ``CSSName``. ``value`` may also be a tuple of
(value, priority), e.g. style['color'] = ('red', 'important')
``CSSName`` will be always normalized.
"""
priority = None
if isinstance(value, tuple):
value, priority = value
return self.setProperty(CSSName, value, priority)
def __delitem__(self, CSSName):
"""Delete property ``CSSName`` from this declaration.
If property is not in this declaration return u'' just like
removeProperty.
``CSSName`` will be always normalized.
"""
return self.removeProperty(CSSName)
def __setattr__(self, n, v):
"""Prevent setting of unknown properties on CSSStyleDeclaration
which would not work anyway. For these
``CSSStyleDeclaration.setProperty`` MUST be called explicitly!
TODO:
implementation of known is not really nice, any alternative?
"""
known = [
'_tokenizer',
'_log',
'_ttypes',
'_seq',
'seq',
'parentRule',
'_parentRule',
'cssText',
'valid',
'wellformed',
'validating',
'_readonly',
'_profiles',
'_validating',
]
known.extend(CSS2Properties._properties)
if n in known:
super().__setattr__(n, v)
else:
raise AttributeError(
'Unknown CSS Property, '
'``CSSStyleDeclaration.setProperty("%s", '
'...)`` MUST be used.' % n
)
def __repr__(self):
return "cssutils.css.{}(cssText={!r})".format(
self.__class__.__name__,
self.getCssText(separator=' '),
)
def __str__(self):
return f""
def __nnames(self):
"""Return iterator for all different names in order as set
if names are set twice the last one is used (double reverse!)
"""
names = []
for item in reversed(self.seq):
val = item.value
if isinstance(val, Property) and val.name not in names:
names.append(val.name)
return reversed(names)
# overwritten accessor functions for CSS2Properties' properties
def _getP(self, CSSName):
"""(DOM CSS2Properties) Overwritten here and effectively the same as
``self.getPropertyValue(CSSname)``.
Parameter is in CSSname format ('font-style'), see CSS2Properties.
Example::
>>> style = CSSStyleDeclaration(cssText='font-style:italic;')
>>> print(style.fontStyle)
italic
"""
return self.getPropertyValue(CSSName)
def _setP(self, CSSName, value):
"""(DOM CSS2Properties) Overwritten here and effectively the same as
``self.setProperty(CSSname, value)``.
Only known CSS2Properties may be set this way, otherwise an
AttributeError is raised.
For these unknown properties ``setPropertyValue(CSSname, value)``
has to be called explicitly.
Also setting the priority of properties needs to be done with a
call like ``setPropertyValue(CSSname, value, priority)``.
Example::
>>> style = CSSStyleDeclaration()
>>> style.fontStyle = 'italic'
>>> # or
>>> style.setProperty('font-style', 'italic', '!important')
"""
self.setProperty(CSSName, value)
# TODO: Shorthand ones
def _delP(self, CSSName):
"""(cssutils only) Overwritten here and effectively the same as
``self.removeProperty(CSSname)``.
Example::
>>> style = CSSStyleDeclaration(cssText='font-style:italic;')
>>> del style.fontStyle
>>> print(style.fontStyle)
"""
self.removeProperty(CSSName)
def children(self):
"""Generator yielding any known child in this declaration including
*all* properties, comments or CSSUnknownrules.
"""
for item in self._seq:
yield item.value
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_css_CSSStyleDeclaration(self)
def _setCssText(self, cssText):
"""Setting this attribute will result in the parsing of the new value
and resetting of all the properties in the declaration block
including the removal or addition of properties.
:exceptions:
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this declaration is readonly or a property is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
"""
self._checkReadonly()
tokenizer = self._tokenize2(cssText)
def ident(expected, seq, token, tokenizer=None):
# a property
tokens = self._tokensupto2(tokenizer, starttoken=token, semicolon=True)
if self._tokenvalue(tokens[-1]) == ';':
tokens.pop()
property = Property(parent=self)
property.cssText = tokens
if property.wellformed:
seq.append(property, 'Property')
else:
self._log.error(
'CSSStyleDeclaration: Syntax Error in '
'Property: %s' % self._valuestr(tokens)
)
# does not matter in this case
return expected
def unexpected(expected, seq, token, tokenizer=None):
# error, find next ; or } to omit upto next property
ignored = self._tokenvalue(token) + self._valuestr(
self._tokensupto2(tokenizer, propertyvalueendonly=True)
)
self._log.error(
'CSSStyleDeclaration: Unexpected token, ignoring ' 'upto %r.' % ignored,
token,
)
# does not matter in this case
return expected
def char(expected, seq, token, tokenizer=None):
# a standalone ; or error...
if self._tokenvalue(token) == ';':
self._log.info(
'CSSStyleDeclaration: Stripped standalone semicolon'
': %s' % self._valuestr([token]),
neverraise=True,
)
return expected
else:
return unexpected(expected, seq, token, tokenizer)
# [Property: Value;]* Property: Value?
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected=None,
seq=newseq,
tokenizer=tokenizer,
productions={'IDENT': ident, 'CHAR': char},
default=unexpected,
)
# wellformed set by parse
for item in newseq:
item.value._parent = self
# do not check wellformed as invalid things are removed anyway
self._setSeq(newseq)
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) A parsable textual representation of the "
"declaration block excluding the surrounding curly "
"braces.",
)
def getCssText(self, separator=None):
"""
:returns:
serialized property cssText, each property separated by
given `separator` which may e.g. be ``u''`` to be able to use
cssText directly in an HTML style attribute. ``;`` is part of
each property (except the last one) and **cannot** be set with
separator!
"""
return cssutils.ser.do_css_CSSStyleDeclaration(self, separator)
def _setParentRule(self, parentRule):
self._parentRule = parentRule
# for x in self.children():
# x.parent = self
parentRule = property(
lambda self: self._parentRule,
_setParentRule,
doc="(DOM) The CSS rule that contains this declaration block or "
"None if this CSSStyleDeclaration is not attached to a CSSRule.",
)
def getProperties(self, name=None, all=False):
"""
:param name:
optional `name` of properties which are requested.
Only properties with this **always normalized** `name` are returned.
If `name` is ``None`` all properties are returned (at least one for
each set name depending on parameter `all`).
:param all:
if ``False`` (DEFAULT) only the effective properties are returned.
If name is given a list with only one property is returned.
if ``True`` all properties including properties set multiple times
with different values or priorities for different UAs are returned.
The order of the properties is fully kept as in the original
stylesheet.
:returns:
a list of :class:`~cssutils.css.Property` objects set in
this declaration.
"""
if name and not all:
# single prop but list
p = self.getProperty(name)
if p:
return [p]
else:
return []
elif not all:
# effective Properties in name order
return [self.getProperty(name) for name in self.__nnames()]
else:
# all properties or all with this name
nname = self._normalize(name)
properties = []
for item in self.seq:
val = item.value
if isinstance(val, Property) and ((not nname) or (val.name == nname)):
properties.append(val)
return properties
def getProperty(self, name, normalize=True):
r"""
:param name:
of the CSS property, always lowercase (even if not normalized)
:param normalize:
if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
escapes) so "color", "COLOR" or "C\olor" will all be equivalent
If ``False`` may return **NOT** the effective value but the
effective for the unnormalized name.
:returns:
the effective :class:`~cssutils.css.Property` object.
"""
nname = self._normalize(name)
found = None
for item in reversed(self.seq):
val = item.value
if isinstance(val, Property):
if (normalize and nname == val.name) or name == val.literalname:
if val.priority:
return val
elif not found:
found = val
return found
def getPropertyCSSValue(self, name, normalize=True):
r"""
:param name:
of the CSS property, always lowercase (even if not normalized)
:param normalize:
if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
escapes) so "color", "COLOR" or "C\olor" will all be equivalent
If ``False`` may return **NOT** the effective value but the
effective for the unnormalized name.
:returns:
``~cssutils.css.CSSValue``, the value of the effective
property if it has been explicitly set for this declaration block.
(DOM)
Used to retrieve the object representation of the value of a CSS
property if it has been explicitly set within this declaration
block. Returns None if the property has not been set.
(This method returns None if the property is a shorthand
property. Shorthand property values can only be accessed and
modified as strings, using the getPropertyValue and setProperty
methods.)
**cssutils currently always returns a CSSValue if the property is
set.**
for more on shorthand properties see
http://www.dustindiaz.com/css-shorthand/
"""
nname = self._normalize(name)
if nname in self._SHORTHANDPROPERTIES:
self._log.info(
'CSSValue for shorthand property "%s" should be '
'None, this may be implemented later.' % nname,
neverraise=True,
)
p = self.getProperty(name, normalize)
if p:
return p.propertyValue
else:
return None
def getPropertyValue(self, name, normalize=True, default=''):
r"""
:param name:
of the CSS property, always lowercase (even if not normalized)
:param normalize:
if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
escapes) so "color", "COLOR" or "C\olor" will all be equivalent
If ``False`` may return **NOT** the effective value but the
effective for the unnormalized name.
:param default:
value to be returned if the property has not been set.
:returns:
the value of the effective property if it has been explicitly set
for this declaration block. Returns `default` if the
property has not been set.
"""
p = self.getProperty(name, normalize)
if p:
return p.value
else:
return default
def getPropertyPriority(self, name, normalize=True):
r"""
:param name:
of the CSS property, always lowercase (even if not normalized)
:param normalize:
if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
escapes) so "color", "COLOR" or "C\olor" will all be equivalent
If ``False`` may return **NOT** the effective value but the
effective for the unnormalized name.
:returns:
the priority of the effective CSS property (e.g. the
"important" qualifier) if the property has been explicitly set in
this declaration block. The empty string if none exists.
"""
p = self.getProperty(name, normalize)
if p:
return p.priority
else:
return ''
def removeProperty(self, name, normalize=True):
r"""
(DOM)
Used to remove a CSS property if it has been explicitly set within
this declaration block.
:param name:
of the CSS property
:param normalize:
if ``True`` (DEFAULT) name will be normalized (lowercase, no simple
escapes) so "color", "COLOR" or "C\olor" will all be equivalent.
The effective Property value is returned and *all* Properties
with ``Property.name == name`` are removed.
If ``False`` may return **NOT** the effective value but the
effective for the unnormalized `name` only. Also only the
Properties with the literal name `name` are removed.
:returns:
the value of the property if it has been explicitly set for
this declaration block. Returns the empty string if the property
has not been set or the property name does not correspond to a
known CSS property
:exceptions:
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this declaration is readonly or the property is
readonly.
"""
self._checkReadonly()
r = self.getPropertyValue(name, normalize=normalize)
newseq = self._tempSeq()
if normalize:
# remove all properties with name == nname
nname = self._normalize(name)
for item in self.seq:
if not (isinstance(item.value, Property) and item.value.name == nname):
newseq.appendItem(item)
else:
# remove all properties with literalname == name
for item in self.seq:
if not (
isinstance(item.value, Property) and item.value.literalname == name
):
newseq.appendItem(item)
self._setSeq(newseq)
return r
def setProperty(self, name, value=None, priority='', normalize=True, replace=True):
r"""(DOM) Set a property value and priority within this declaration
block.
:param name:
of the CSS property to set (in W3C DOM the parameter is called
"propertyName"), always lowercase (even if not normalized)
If a property with this `name` is present it will be reset.
cssutils also allowed `name` to be a
:class:`~cssutils.css.Property` object, all other
parameter are ignored in this case
:param value:
the new value of the property, ignored if `name` is a Property.
:param priority:
the optional priority of the property (e.g. "important"),
ignored if `name` is a Property.
:param normalize:
if True (DEFAULT) `name` will be normalized (lowercase, no simple
escapes) so "color", "COLOR" or "C\olor" will all be equivalent
:param replace:
if True (DEFAULT) the given property will replace a present
property. If False a new property will be added always.
The difference to `normalize` is that two or more properties with
the same name may be set, useful for e.g. stuff like::
background: red;
background: rgba(255, 0, 0, 0.5);
which defines the same property but only capable UAs use the last
property value, older ones use the first value.
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified value has a syntax error and is
unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this declaration is readonly or the property is
readonly.
"""
self._checkReadonly()
if isinstance(name, Property):
newp = name
name = newp.literalname
elif not value:
# empty string or None effectively removed property
return self.removeProperty(name)
else:
newp = Property(name, value, priority, parent=self)
if newp.wellformed:
if replace:
# check if update
nname = self._normalize(name)
properties = self.getProperties(name, all=(not normalize))
for property in reversed(properties):
if normalize and property.name == nname:
property.propertyValue = newp.propertyValue.cssText
property.priority = newp.priority
return
elif property.literalname == name:
property.propertyValue = newp.propertyValue.cssText
property.priority = newp.priority
return
# not yet set or forced omit replace
newp.parent = self
self.seq._readonly = False
self.seq.append(newp, 'Property')
self.seq._readonly = True
else:
self._log.warn(f'Invalid Property: {name}: {value} {priority}')
def item(self, index):
"""(DOM) Retrieve the properties that have been explicitly set in
this declaration block. The order of the properties retrieved using
this method does not have to be the order in which they were set.
This method can be used to iterate over all properties in this
declaration block.
:param index:
of the property to retrieve, negative values behave like
negative indexes on Python lists, so -1 is the last element
:returns:
the name of the property at this ordinal position. The
empty string if no property exists at this position.
**ATTENTION:**
Only properties with different names are counted. If two
properties with the same name are present in this declaration
only the effective one is included.
:meth:`item` and :attr:`length` work on the same set here.
"""
names = list(self.__nnames())
try:
return names[index]
except IndexError:
return ''
length = property(
lambda self: len(list(self.__nnames())),
doc="(DOM) The number of distinct properties that have "
"been explicitly in this declaration block. The "
"range of valid indices is 0 to length-1 inclusive. "
"These are properties with a different ``name`` "
"only. :meth:`item` and :attr:`length` work on the "
"same set here.",
)
def _getValidating(self):
try:
# CSSParser.parseX() sets validating of stylesheet
return self.parentRule.parentStyleSheet.validating
except AttributeError:
# CSSParser.parseStyle() sets validating of declaration
if self._validating is not None:
return self._validating
# default
return True
def _setValidating(self, validating):
self._validating = validating
validating = property(
_getValidating,
_setValidating,
doc="If ``True`` this declaration validates "
"contained properties. The parent StyleSheet "
"validation setting does *always* win though so "
"even if validating is True it may not validate "
"if the StyleSheet defines else!",
)
def _getValid(self):
"""Check each contained property for validity."""
return all(prop.valid for prop in self.getProperties())
valid = property(_getValid, doc='``True`` if each property is valid.')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssstylerule.py 0000644 0001751 0000177 00000022034 14627633762 020647 0 ustar 00runner docker """CSSStyleRule implements DOM Level 2 CSS CSSStyleRule."""
__all__ = ['CSSStyleRule']
import xml.dom
import cssutils
from . import cssrule
from .cssstyledeclaration import CSSStyleDeclaration
from .selectorlist import SelectorList
class CSSStyleRule(cssrule.CSSRule):
"""The CSSStyleRule object represents a ruleset specified (if any) in a CSS
style sheet. It provides access to a declaration block as well as to the
associated group of selectors.
Format::
: selector [ COMMA S* selector ]*
LBRACE S* declaration [ ';' S* declaration ]* '}' S*
;
"""
def __init__(
self,
selectorText=None,
style=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""
:Parameters:
selectorText
string parsed into selectorList
style
string parsed into CSSStyleDeclaration for this CSSStyleRule
readonly
if True allows setting of properties in constructor only
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self.selectorList = SelectorList()
if selectorText:
self.selectorText = selectorText
if style:
self.style = style
else:
self.style = CSSStyleDeclaration()
self._readonly = readonly
def __repr__(self):
if self._namespaces:
st = (self.selectorText, self._namespaces)
else:
st = self.selectorText
return f"cssutils.css.{self.__class__.__name__}(selectorText={st!r}, style={self.style.cssText!r})"
def __str__(self):
return (
""
% (
self.__class__.__name__,
self.selectorText,
self.style.cssText,
self._namespaces,
id(self),
)
)
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSStyleRule(self)
def _setCssText(self, cssText): # noqa: C901
"""
:param cssText:
a parseable string or a tuple of (cssText, dict-of-namespaces)
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if the specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
# might be (cssText, namespaces)
cssText, namespaces = self._splitNamespacesOff(cssText)
try:
# use parent style sheet ones if available
namespaces = self.parentStyleSheet.namespaces
except AttributeError:
pass
tokenizer = self._tokenize2(cssText)
selectortokens = self._tokensupto2(tokenizer, blockstartonly=True)
styletokens = self._tokensupto2(tokenizer, blockendonly=True)
trail = self._nexttoken(tokenizer)
if trail:
self._log.error(
'CSSStyleRule: Trailing content: %s' % self._valuestr(cssText),
token=trail,
)
elif not selectortokens:
self._log.error(
'CSSStyleRule: No selector found: %r' % self._valuestr(cssText)
)
elif self._tokenvalue(selectortokens[0]).startswith('@'):
self._log.error(
'CSSStyleRule: No style rule: %r' % self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
newSelectorList = SelectorList(parentRule=self)
newStyle = CSSStyleDeclaration(parentRule=self)
ok = True
bracetoken = selectortokens.pop()
if self._tokenvalue(bracetoken) != '{':
ok = False
self._log.error(
'CSSStyleRule: No start { of style declaration found: %r'
% self._valuestr(cssText),
bracetoken,
)
elif not selectortokens:
ok = False
self._log.error(
'CSSStyleRule: No selector found: %r.' % self._valuestr(cssText),
bracetoken,
)
# SET
newSelectorList.selectorText = (selectortokens, namespaces)
if not styletokens:
ok = False
self._log.error(
'CSSStyleRule: No style declaration or "}" found: %r'
% self._valuestr(cssText)
)
else:
braceorEOFtoken = styletokens.pop()
val, typ = (
self._tokenvalue(braceorEOFtoken),
self._type(braceorEOFtoken),
)
if val != '}' and typ != 'EOF':
ok = False
self._log.error(
'CSSStyleRule: No "}" after style '
'declaration found: %r' % self._valuestr(cssText)
)
else:
if 'EOF' == typ:
# add again as style needs it
styletokens.append(braceorEOFtoken)
# SET, may raise:
newStyle.cssText = styletokens
if ok:
self.selectorList = newSelectorList
self.style = newStyle
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) The parsable textual representation of this " "rule.",
)
def __getNamespaces(self):
"""Uses children namespaces if not attached to a sheet, else the sheet's
ones."""
try:
return self.parentStyleSheet.namespaces
except AttributeError:
return self.selectorList._namespaces
_namespaces = property(
__getNamespaces,
doc="If this Rule is attached to a CSSStyleSheet "
"the namespaces of that sheet are mirrored "
"here. While the Rule is not attached the "
"namespaces of selectorList are used."
"",
)
def _setSelectorList(self, selectorList):
"""
:param selectorList: A SelectorList which replaces the current
selectorList object
"""
self._checkReadonly()
selectorList._parentRule = self
self._selectorList = selectorList
_selectorList = None
selectorList = property(
lambda self: self._selectorList,
_setSelectorList,
doc="The SelectorList of this rule.",
)
def _setSelectorText(self, selectorText):
"""
wrapper for cssutils SelectorList object
:param selectorText:
of type string, might also be a comma separated list
of selectors
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if the specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
and is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
sl = SelectorList(selectorText=selectorText, parentRule=self)
if sl.wellformed:
self._selectorList = sl
selectorText = property(
lambda self: self._selectorList.selectorText,
_setSelectorText,
doc="(DOM) The textual representation of the " "selector for the rule set.",
)
def _setStyle(self, style):
"""
:param style: A string or CSSStyleDeclaration which replaces the
current style object.
"""
self._checkReadonly()
if isinstance(style, str):
self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
else:
style._parentRule = self
self._style = style
style = property(
lambda self: self._style,
_setStyle,
doc="(DOM) The declaration-block of this rule set.",
)
type = property(
lambda self: self.STYLE_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
wellformed = property(lambda self: self.selectorList.wellformed)
def _getValid(self):
"""Return whether the style declaration is valid."""
return self.style.valid
valid = property(_getValid, doc='``True`` when the style declaration is true.')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssstylesheet.py 0000644 0001751 0000177 00000101721 14627633762 021011 0 ustar 00runner docker """CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet.
Partly also:
- http://dev.w3.org/csswg/cssom/#the-cssstylesheet
- http://www.w3.org/TR/2006/WD-css3-namespace-20060828/
TODO:
- ownerRule and ownerNode
"""
__all__ = ['CSSStyleSheet']
import xml.dom
import cssutils.stylesheets
from cssutils.helper import Deprecated
from cssutils.util import _Namespaces, _readUrl
from .cssrule import CSSRule
from .cssvariablesdeclaration import CSSVariablesDeclaration
class CSSStyleSheet(cssutils.stylesheets.StyleSheet):
"""CSSStyleSheet represents a CSS style sheet.
Format::
stylesheet
: [ CHARSET_SYM S* STRING S* ';' ]?
[S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
[ namespace [S|CDO|CDC]* ]* # according to @namespace WD
[ [ ruleset | media | page ] [S|CDO|CDC]* ]*
``cssRules``
All Rules in this style sheet, a :class:`~cssutils.css.CSSRuleList`.
"""
def __init__(
self,
href=None,
media=None,
title='',
disabled=None,
ownerNode=None,
parentStyleSheet=None,
readonly=False,
ownerRule=None,
validating=True,
):
"""
For parameters see :class:`~cssutils.stylesheets.StyleSheet`
"""
super().__init__(
'text/css',
href,
media,
title,
disabled,
ownerNode,
parentStyleSheet,
validating=validating,
)
self._ownerRule = ownerRule
self.cssRules = cssutils.css.CSSRuleList()
self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log)
self._variables = CSSVariablesDeclaration()
self._readonly = readonly
# used only during setting cssText by parse*()
self.__encodingOverride = None
self._fetcher = None
def __iter__(self):
"Generator which iterates over cssRules."
yield from self._cssRules
def __repr__(self):
if self.media:
mediaText = self.media.mediaText
else:
mediaText = None
return f"cssutils.css.{self.__class__.__name__}(href={self.href!r}, media={mediaText!r}, title={self.title!r})"
def __str__(self):
if self.media:
mediaText = self.media.mediaText
else:
mediaText = None
return (
""
% (
self.__class__.__name__,
self.encoding,
self.href,
mediaText,
self.title,
self.namespaces.namespaces,
id(self),
)
)
def _cleanNamespaces(self):
"Remove all namespace rules with same namespaceURI but last."
rules = self.cssRules
namespaceitems = list(self.namespaces.items())
i = 0
while i < len(rules):
rule = rules[i]
if (
rule.type == rule.NAMESPACE_RULE
and (rule.prefix, rule.namespaceURI) not in namespaceitems
):
self.deleteRule(i)
else:
i += 1
def _getUsedURIs(self):
"Return set of URIs used in the sheet."
useduris = set()
for r1 in self:
if r1.STYLE_RULE == r1.type:
useduris.update(r1.selectorList._getUsedUris())
elif r1.MEDIA_RULE == r1.type:
for r2 in r1:
if r2.type == r2.STYLE_RULE:
useduris.update(r2.selectorList._getUsedUris())
return useduris
@property
def cssRules(self):
"All Rules in this style sheet, a :class:`~cssutils.css.CSSRuleList`."
return self._cssRules
@cssRules.setter
def cssRules(self, cssRules):
"Set new cssRules and update contained rules refs."
cssRules.append = self.insertRule
cssRules.extend = self.insertRule
cssRules.__delitem__ = self.deleteRule
for rule in cssRules:
rule._parentStyleSheet = self
self._cssRules = cssRules
def _getCssText(self):
"Textual representation of the stylesheet (a byte string)."
return cssutils.ser.do_CSSStyleSheet(self)
def _setCssText(self, cssText): # noqa: C901
"""Parse `cssText` and overwrites the whole stylesheet.
:param cssText:
a parseable string or a tuple of (cssText, dict-of-namespaces)
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
If a namespace prefix is found which is not declared.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
"""
self._checkReadonly()
cssText, namespaces = self._splitNamespacesOff(cssText)
tokenizer = self._tokenize2(cssText)
def S(expected, seq, token, tokenizer=None):
# @charset must be at absolute beginning of style sheet
# or 0 for py3
return max(1, expected or 0)
def COMMENT(expected, seq, token, tokenizer=None):
"special: sets parent*"
self.insertRule(cssutils.css.CSSComment([token], parentStyleSheet=self))
# or 0 for py3
return max(1, expected or 0)
def charsetrule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if expected > 0:
self._log.error(
'CSSStylesheet: CSSCharsetRule only allowed '
'at beginning of stylesheet.',
token,
xml.dom.HierarchyRequestErr,
)
return expected
elif rule.wellformed:
self.insertRule(rule)
return 1
def importrule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSImportRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if expected > 1:
self._log.error(
'CSSStylesheet: CSSImportRule not allowed ' 'here.',
token,
xml.dom.HierarchyRequestErr,
)
return expected
elif rule.wellformed:
self.insertRule(rule)
return 1
def namespacerule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSNamespaceRule(
cssText=self._tokensupto2(tokenizer, token), parentStyleSheet=self
)
if expected > 2:
self._log.error(
'CSSStylesheet: CSSNamespaceRule not allowed ' 'here.',
token,
xml.dom.HierarchyRequestErr,
)
return expected
elif rule.wellformed:
if rule.prefix not in self.namespaces:
# add new if not same prefix
self.insertRule(rule, _clean=False)
else:
# same prefix => replace namespaceURI
for r in self.cssRules.rulesOfType(rule.NAMESPACE_RULE):
if r.prefix == rule.prefix:
r._replaceNamespaceURI(rule.namespaceURI)
self._namespaces[rule.prefix] = rule.namespaceURI
return 2
def variablesrule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSVariablesRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if expected > 2:
self._log.error(
'CSSStylesheet: CSSVariablesRule not allowed ' 'here.',
token,
xml.dom.HierarchyRequestErr,
)
return expected
elif rule.wellformed:
self.insertRule(rule)
self._updateVariables()
return 2
def fontfacerule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if rule.wellformed:
self.insertRule(rule)
return 3
def mediarule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSMediaRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if rule.wellformed:
self.insertRule(rule)
return 3
def pagerule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSPageRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if rule.wellformed:
self.insertRule(rule)
return 3
def unknownrule(expected, seq, token, tokenizer):
# parse and consume tokens in any case
if token[1] in cssutils.css.MarginRule.margins:
self._log.error(
'CSSStylesheet: MarginRule out CSSPageRule.', token, neverraise=True
)
rule = cssutils.css.MarginRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
else:
self._log.warn(
'CSSStylesheet: Unknown @rule found.', token, neverraise=True
)
rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if rule.wellformed:
self.insertRule(rule)
# or 0 for py3
return max(1, expected or 0)
def ruleset(expected, seq, token, tokenizer):
# parse and consume tokens in any case
rule = cssutils.css.CSSStyleRule(parentStyleSheet=self)
rule.cssText = self._tokensupto2(tokenizer, token)
if rule.wellformed:
self.insertRule(rule)
return 3
# save for possible reset
oldCssRules = self.cssRules
oldNamespaces = self._namespaces
self.cssRules = cssutils.css.CSSRuleList()
# simple during parse
self._namespaces = namespaces
self._variables = CSSVariablesDeclaration()
# not used?!
newseq = []
# ['CHARSET', 'IMPORT', ('VAR', NAMESPACE'), ('PAGE', 'MEDIA', ruleset)]
wellformed, expected = self._parse(
0,
newseq,
tokenizer,
{
'S': S,
'COMMENT': COMMENT,
'CDO': lambda *ignored: None,
'CDC': lambda *ignored: None,
'CHARSET_SYM': charsetrule,
'FONT_FACE_SYM': fontfacerule,
'IMPORT_SYM': importrule,
'NAMESPACE_SYM': namespacerule,
'PAGE_SYM': pagerule,
'MEDIA_SYM': mediarule,
'VARIABLES_SYM': variablesrule,
'ATKEYWORD': unknownrule,
},
default=ruleset,
)
if wellformed:
# use proper namespace object
self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log)
self._cleanNamespaces()
else:
# reset
self._cssRules = oldCssRules
self._namespaces = oldNamespaces
self._updateVariables()
self._cleanNamespaces()
cssText = property(
_getCssText,
_setCssText,
"Textual representation of the stylesheet (a byte string)",
)
def _resolveImport(self, url):
"""Read (encoding, enctype, decodedContent) from `url` for @import
sheets."""
try:
# only available during parsing of a complete sheet
parentEncoding = self.__newEncoding
except AttributeError:
try:
# explicit @charset
parentEncoding = self._cssRules[0].encoding
except (IndexError, AttributeError):
# default not UTF-8 but None!
parentEncoding = None
return _readUrl(
url,
fetcher=self._fetcher,
overrideEncoding=self.__encodingOverride,
parentEncoding=parentEncoding,
)
def _setCssTextWithEncodingOverride(
self, cssText, encodingOverride=None, encoding=None
):
"""Set `cssText` but use `encodingOverride` to overwrite detected
encoding. This is used by parse and @import during setting of cssText.
If `encoding` is given use this but do not save as `encodingOverride`.
"""
if encodingOverride:
# encoding during resolving of @import
self.__encodingOverride = encodingOverride
if encoding:
# save for nested @import
self.__newEncoding = encoding
self.cssText = cssText
if encodingOverride:
# set encodingOverride explicit again!
self.encoding = self.__encodingOverride
# del?
self.__encodingOverride = None
elif encoding:
# may e.g. be httpEncoding
self.encoding = encoding
try:
del self.__newEncoding
except AttributeError:
pass
def _setFetcher(self, fetcher=None):
"""Set @import URL loader, if None the default is used."""
self._fetcher = fetcher
def _getEncoding(self):
"""Encoding set in :class:`~cssutils.css.CSSCharsetRule` or if ``None``
resulting in default ``utf-8`` encoding being used."""
try:
return self._cssRules[0].encoding
except (IndexError, AttributeError):
return 'utf-8'
def _setEncoding(self, encoding):
"""Set `encoding` of charset rule if present in sheet or insert a new
:class:`~cssutils.css.CSSCharsetRule` with given `encoding`.
If `encoding` is None removes charsetrule if present resulting in
default encoding of utf-8.
"""
try:
rule = self._cssRules[0]
except IndexError:
rule = None
if rule and rule.CHARSET_RULE == rule.type:
if encoding:
rule.encoding = encoding
else:
self.deleteRule(0)
elif encoding:
self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0)
encoding = property(
_getEncoding,
_setEncoding,
"(cssutils) Reflect encoding of an @charset rule or 'utf-8' "
"(default) if set to ``None``",
)
@property
def namespaces(self):
"""All Namespaces used in this CSSStyleSheet."""
return self._namespaces
def _updateVariables(self):
"""Updates self._variables, called when @import or @variables rules
is added to sheet.
"""
for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
s = r.styleSheet
if s:
for var in s.variables:
self._variables.setVariable(var, s.variables[var])
# for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
# for vr in r.styleSheet.cssRules.rulesOfType(CSSRule.VARIABLES_RULE):
# for var in vr.variables:
# self._variables.setVariable(var, vr.variables[var])
for vr in self.cssRules.rulesOfType(CSSRule.VARIABLES_RULE):
for var in vr.variables:
self._variables.setVariable(var, vr.variables[var])
variables = property(
lambda self: self._variables,
doc="A :class:`cssutils.css.CSSVariablesDeclaration` "
"containing all available variables in this "
"CSSStyleSheet including the ones defined in "
"imported sheets.",
)
def add(self, rule):
"""Add `rule` to style sheet at appropriate position.
Same as ``insertRule(rule, inOrder=True)``.
"""
return self.insertRule(rule, index=None, inOrder=True)
def deleteRule(self, index):
"""Delete rule at `index` from the style sheet.
:param index:
The `index` of the rule to be removed from the StyleSheet's rule
list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is
raised but rules for normal Python lists are used. E.g.
``deleteRule(-1)`` removes the last rule in cssRules.
`index` may also be a CSSRule object which will then be removed
from the StyleSheet.
:exceptions:
- :exc:`~xml.dom.IndexSizeErr`:
Raised if the specified index does not correspond to a rule in
the style sheet's rule list.
- :exc:`~xml.dom.NamespaceErr`:
Raised if removing this rule would result in an invalid StyleSheet
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this style sheet is readonly.
"""
self._checkReadonly()
if isinstance(index, CSSRule):
for i, r in enumerate(self.cssRules):
if index == r:
index = i
break
else:
raise xml.dom.IndexSizeErr(
"CSSStyleSheet: Not a rule in"
" this sheets'a cssRules list: %s" % index
)
try:
rule = self._cssRules[index]
except IndexError as err:
raise xml.dom.IndexSizeErr(
'CSSStyleSheet: %s is not a valid index in the rulelist of '
'length %i' % (index, self._cssRules.length)
) from err
else:
if rule.type == rule.NAMESPACE_RULE:
# check all namespacerules if used
uris = [r.namespaceURI for r in self if r.type == r.NAMESPACE_RULE]
useduris = self._getUsedURIs()
if rule.namespaceURI in useduris and uris.count(rule.namespaceURI) == 1:
raise xml.dom.NoModificationAllowedErr(
'CSSStyleSheet: NamespaceURI defined in this rule is '
'used, cannot remove.'
)
return
rule._parentStyleSheet = None # detach
del self._cssRules[index] # delete from StyleSheet
def insertRule(self, rule, index=None, inOrder=False, _clean=True): # noqa: C901
"""
Used to insert a new rule into the style sheet. The new rule now
becomes part of the cascade.
:param rule:
a parsable DOMString, in cssutils also a
:class:`~cssutils.css.CSSRule` or :class:`~cssutils.css.CSSRuleList`
:param index:
of the rule before the new rule will be inserted.
If the specified `index` is equal to the length of the
StyleSheet's rule collection, the rule will be added to the end
of the style sheet.
If `index` is not given or ``None`` rule will be appended to rule
list.
:param inOrder:
if ``True`` the rule will be put to a proper location while
ignoring `index` and without raising
:exc:`~xml.dom.HierarchyRequestErr`.
The resulting index is returned nevertheless.
:returns: The index within the style sheet's rule collection
:Exceptions:
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at the specified `index`
e.g. if an @import rule is inserted after a standard rule set
or other at-rule.
- :exc:`~xml.dom.IndexSizeErr`:
Raised if the specified `index` is not a valid insertion point.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this style sheet is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified rule has a syntax error and is
unparsable.
"""
self._checkReadonly()
# check position
if index is None:
index = len(self._cssRules)
elif index < 0 or index > self._cssRules.length:
raise xml.dom.IndexSizeErr(
'CSSStyleSheet: Invalid index %s for CSSRuleList with a '
'length of %s.' % (index, self._cssRules.length)
)
return
if isinstance(rule, str):
# init a temp sheet which has the same properties as self
tempsheet = CSSStyleSheet(
href=self.href,
media=self.media,
title=self.title,
parentStyleSheet=self.parentStyleSheet,
ownerRule=self.ownerRule,
)
tempsheet._ownerNode = self.ownerNode
tempsheet._fetcher = self._fetcher
# prepend encoding if in this sheet to be able to use it in
# @import rules encoding resolution
# do not add if new rule startswith "@charset" (which is exact!)
if not rule.startswith('@charset') and (
self._cssRules
and self._cssRules[0].type == self._cssRules[0].CHARSET_RULE
):
# rule 0 is @charset!
newrulescount, newruleindex = 2, 1
rule = self._cssRules[0].cssText + rule
else:
newrulescount, newruleindex = 1, 0
# parse the new rule(s)
tempsheet.cssText = (rule, self._namespaces)
if len(tempsheet.cssRules) != newrulescount or (
not isinstance(tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)
):
self._log.error('CSSStyleSheet: Not a CSSRule: %s' % rule)
return
rule = tempsheet.cssRules[newruleindex]
rule._parentStyleSheet = None # done later?
# TODO:
# tempsheet._namespaces = self._namespaces
# variables?
elif isinstance(rule, cssutils.css.CSSRuleList):
# insert all rules
for i, r in enumerate(rule):
self.insertRule(r, index + i)
return index
if not rule.wellformed:
self._log.error('CSSStyleSheet: Invalid rules cannot be added.')
return
# CHECK HIERARCHY
# @charset
if rule.type == rule.CHARSET_RULE:
if inOrder:
index = 0
# always first and only
if self._cssRules and self._cssRules[0].type == rule.CHARSET_RULE:
self._cssRules[0].encoding = rule.encoding
else:
self._cssRules.insert(0, rule)
elif index != 0 or (
self._cssRules and self._cssRules[0].type == rule.CHARSET_RULE
):
self._log.error(
'CSSStylesheet: @charset only allowed once at the'
' beginning of a stylesheet.',
error=xml.dom.HierarchyRequestErr,
)
return
else:
self._cssRules.insert(index, rule)
# @unknown or comment
elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder:
if (
index == 0
and self._cssRules
and self._cssRules[0].type == rule.CHARSET_RULE
):
self._log.error(
'CSSStylesheet: @charset must be the first rule.',
error=xml.dom.HierarchyRequestErr,
)
return
else:
self._cssRules.insert(index, rule)
# @import
elif rule.type == rule.IMPORT_RULE:
if inOrder:
# automatic order
if rule.type in (r.type for r in self):
# find last of this type
for i, r in enumerate(reversed(self._cssRules)):
if r.type == rule.type:
index = len(self._cssRules) - i
break
else:
# find first point to insert
if self._cssRules and self._cssRules[0].type in (
rule.CHARSET_RULE,
rule.COMMENT,
):
index = 1
else:
index = 0
else:
# after @charset
if (
index == 0
and self._cssRules
and self._cssRules[0].type == rule.CHARSET_RULE
):
self._log.error(
'CSSStylesheet: Found @charset at index 0.',
error=xml.dom.HierarchyRequestErr,
)
return
# before @namespace @variables @page @font-face @media stylerule
for r in self._cssRules[:index]:
if r.type in (
r.NAMESPACE_RULE,
r.VARIABLES_RULE,
r.MEDIA_RULE,
r.PAGE_RULE,
r.STYLE_RULE,
r.FONT_FACE_RULE,
):
self._log.error(
'CSSStylesheet: Cannot insert @import here,'
' found @namespace, @variables, @media, @page or'
' CSSStyleRule before index %s.' % index,
error=xml.dom.HierarchyRequestErr,
)
return
self._cssRules.insert(index, rule)
self._updateVariables()
# @namespace
elif rule.type == rule.NAMESPACE_RULE:
if inOrder:
if rule.type in (r.type for r in self):
# find last of this type
for i, r in enumerate(reversed(self._cssRules)):
if r.type == rule.type:
index = len(self._cssRules) - i
break
else:
# find first point to insert
for i, r in enumerate(self._cssRules):
if r.type in (
r.VARIABLES_RULE,
r.MEDIA_RULE,
r.PAGE_RULE,
r.STYLE_RULE,
r.FONT_FACE_RULE,
r.UNKNOWN_RULE,
r.COMMENT,
):
index = i # before these
break
else:
# after @charset and @import
for r in self._cssRules[index:]:
if r.type in (r.CHARSET_RULE, r.IMPORT_RULE):
self._log.error(
'CSSStylesheet: Cannot insert @namespace here,'
' found @charset or @import after index %s.' % index,
error=xml.dom.HierarchyRequestErr,
)
return
# before @variables @media @page @font-face and stylerule
for r in self._cssRules[:index]:
if r.type in (
r.VARIABLES_RULE,
r.MEDIA_RULE,
r.PAGE_RULE,
r.STYLE_RULE,
r.FONT_FACE_RULE,
):
self._log.error(
'CSSStylesheet: Cannot insert @namespace here,'
' found @variables, @media, @page or CSSStyleRule'
' before index %s.' % index,
error=xml.dom.HierarchyRequestErr,
)
return
if not (
rule.prefix in self.namespaces
and self.namespaces[rule.prefix] == rule.namespaceURI
):
# no doublettes
self._cssRules.insert(index, rule)
if _clean:
self._cleanNamespaces()
# @variables
elif rule.type == rule.VARIABLES_RULE:
if inOrder:
if rule.type in (r.type for r in self):
# find last of this type
for i, r in enumerate(reversed(self._cssRules)):
if r.type == rule.type:
index = len(self._cssRules) - i
break
else:
# find first point to insert
for i, r in enumerate(self._cssRules):
if r.type in (
r.MEDIA_RULE,
r.PAGE_RULE,
r.STYLE_RULE,
r.FONT_FACE_RULE,
r.UNKNOWN_RULE,
r.COMMENT,
):
index = i # before these
break
else:
# after @charset @import @namespace
for r in self._cssRules[index:]:
if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE):
self._log.error(
'CSSStylesheet: Cannot insert @variables here,'
' found @charset, @import or @namespace after'
' index %s.' % index,
error=xml.dom.HierarchyRequestErr,
)
return
# before @media @page @font-face and stylerule
for r in self._cssRules[:index]:
if r.type in (
r.MEDIA_RULE,
r.PAGE_RULE,
r.STYLE_RULE,
r.FONT_FACE_RULE,
):
self._log.error(
'CSSStylesheet: Cannot insert @variables here,'
' found @media, @page or CSSStyleRule'
' before index %s.' % index,
error=xml.dom.HierarchyRequestErr,
)
return
self._cssRules.insert(index, rule)
self._updateVariables()
# all other where order is not important
else:
if inOrder:
# simply add to end as no specific order
self._cssRules.append(rule)
index = len(self._cssRules) - 1
else:
for r in self._cssRules[index:]:
if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE):
self._log.error(
'CSSStylesheet: Cannot insert rule here, found '
'@charset, @import or @namespace before index %s.' % index,
error=xml.dom.HierarchyRequestErr,
)
return
self._cssRules.insert(index, rule)
# post settings
rule._parentStyleSheet = self
if rule.IMPORT_RULE == rule.type and not rule.hrefFound:
# try loading the imported sheet which has new relative href now
rule.href = rule.href
return index
ownerRule = property(
lambda self: self._ownerRule,
doc='A ref to an @import rule if it is imported, ' 'else ``None``.',
)
def _getValid(self):
"""Check if each contained rule is valid."""
for rule in self.cssRules:
# Not all rules can be checked for validity
if hasattr(rule, 'valid') and not rule.valid:
return False
return True
valid = property(_getValid, doc='``True`` if all contained rules are valid')
@Deprecated('Use ``cssutils.setSerializer(serializer)`` instead.')
def setSerializer(self, cssserializer):
"""Set the cssutils global Serializer used for all output."""
if isinstance(cssserializer, cssutils.CSSSerializer):
cssutils.ser = cssserializer
else:
raise ValueError(
'Serializer must be an instance of ' 'cssutils.CSSSerializer.'
)
@Deprecated('Set pref in ``cssutils.ser.prefs`` instead.')
def setSerializerPref(self, pref, value):
"""Set a Preference of CSSSerializer used for output.
See :class:`cssutils.serialize.Preferences` for possible
preferences to be set.
"""
cssutils.ser.prefs.__setattr__(pref, value)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssunknownrule.py 0000644 0001751 0000177 00000020016 14627633762 021204 0 ustar 00runner docker """CSSUnknownRule implements DOM Level 2 CSS CSSUnknownRule."""
__all__ = ['CSSUnknownRule']
import xml.dom
import cssutils
from . import cssrule
class CSSUnknownRule(cssrule.CSSRule):
"""
Represents an at-rule not supported by this user agent, so in
effect all other at-rules not defined in cssutils.
Format::
@xxx until ';' or block {...}
"""
def __init__(
self, cssText='', parentRule=None, parentStyleSheet=None, readonly=False
):
"""
:param cssText:
of type string
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = None
if cssText:
self.cssText = cssText
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(cssText={self.cssText!r})"
def __str__(self):
return f""
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSUnknownRule(self)
def _setCssText(self, cssText): # noqa: C901
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
attoken = self._nexttoken(tokenizer, None)
if not attoken or self._type(attoken) != self._prods.ATKEYWORD:
self._log.error(
'CSSUnknownRule: No CSSUnknownRule found: %s' % self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
# for closures: must be a mutable
new = {'nesting': [], 'wellformed': True} # {} [] or ()
def CHAR(expected, seq, token, tokenizer=None):
type_, val, line, col = token
if expected != 'EOF':
if val in '{[(':
new['nesting'].append(val)
elif val in '}])':
opening = {'}': '{', ']': '[', ')': '('}[val]
try:
if new['nesting'][-1] == opening:
new['nesting'].pop()
else:
raise IndexError()
except IndexError:
new['wellformed'] = False
self._log.error(
'CSSUnknownRule: Wrong nesting of ' '{, [ or (.',
token=token,
)
if val in '};' and not new['nesting']:
expected = 'EOF'
seq.append(val, type_, line=line, col=col)
return expected
else:
new['wellformed'] = False
self._log.error(
'CSSUnknownRule: Expected end of rule.', token=token
)
return expected
def FUNCTION(expected, seq, token, tokenizer=None):
# handled as opening (
type_, val, line, col = token
val = self._tokenvalue(token)
if expected != 'EOF':
new['nesting'].append('(')
seq.append(val, type_, line=line, col=col)
return expected
else:
new['wellformed'] = False
self._log.error(
'CSSUnknownRule: Expected end of rule.', token=token
)
return expected
def EOF(expected, seq, token, tokenizer=None):
"close all blocks and return 'EOF'"
for x in reversed(new['nesting']):
closing = {'{': '}', '[': ']', '(': ')'}[x]
seq.append(closing, closing)
new['nesting'] = []
return 'EOF'
def INVALID(expected, seq, token, tokenizer=None):
# makes rule invalid
self._log.error(
'CSSUnknownRule: Bad syntax.', token=token, error=xml.dom.SyntaxErr
)
new['wellformed'] = False
return expected
def STRING(expected, seq, token, tokenizer=None):
type_, val, line, col = token
val = self._stringtokenvalue(token)
if expected != 'EOF':
seq.append(val, type_, line=line, col=col)
return expected
else:
new['wellformed'] = False
self._log.error(
'CSSUnknownRule: Expected end of rule.', token=token
)
return expected
def URI(expected, seq, token, tokenizer=None):
type_, val, line, col = token
val = self._uritokenvalue(token)
if expected != 'EOF':
seq.append(val, type_, line=line, col=col)
return expected
else:
new['wellformed'] = False
self._log.error(
'CSSUnknownRule: Expected end of rule.', token=token
)
return expected
def default(expected, seq, token, tokenizer=None):
type_, val, line, col = token
if expected != 'EOF':
seq.append(val, type_, line=line, col=col)
return expected
else:
new['wellformed'] = False
self._log.error(
'CSSUnknownRule: Expected end of rule.', token=token
)
return expected
# unknown : ATKEYWORD S* ... ; | }
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected=None,
seq=newseq,
tokenizer=tokenizer,
productions={
'CHAR': CHAR,
'EOF': EOF,
'FUNCTION': FUNCTION,
'INVALID': INVALID,
'STRING': STRING,
'URI': URI,
'S': default, # overwrite default default!
},
default=default,
new=new,
)
# wellformed set by parse
wellformed = wellformed and new['wellformed']
# post conditions
if expected != 'EOF':
wellformed = False
self._log.error(
'CSSUnknownRule: No ending ";" or "}" found: '
'%r' % self._valuestr(cssText)
)
elif new['nesting']:
wellformed = False
self._log.error(
'CSSUnknownRule: Unclosed "{", "[" or "(": %r'
% self._valuestr(cssText)
)
# set all
if wellformed:
self.atkeyword = self._tokenvalue(attoken)
self._setSeq(newseq)
cssText = property(
fget=_getCssText,
fset=_setCssText,
doc="(DOM) The parsable textual representation.",
)
type = property(
lambda self: self.UNKNOWN_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
wellformed = property(lambda self: bool(self.atkeyword))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssvalue.py 0000644 0001751 0000177 00000135136 14627633762 017743 0 ustar 00runner docker """CSSValue related classes
- CSSValue implements DOM Level 2 CSS CSSValue
- CSSPrimitiveValue implements DOM Level 2 CSS CSSPrimitiveValue
- CSSValueList implements DOM Level 2 CSS CSSValueList
"""
__all__ = ['CSSValue', 'CSSPrimitiveValue', 'CSSValueList', 'RGBColor', 'CSSVariable']
import math
import re
import xml.dom
import cssutils
import cssutils.helper
from cssutils.prodparser import Choice, PreDef, Prod, ProdParser, Sequence
class CSSValue(cssutils.util._NewBase):
"""The CSSValue interface represents a simple or a complex value.
A CSSValue object only occurs in a context of a CSS property.
"""
# The value is inherited and the cssText contains "inherit".
CSS_INHERIT = 0
# The value is a CSSPrimitiveValue.
CSS_PRIMITIVE_VALUE = 1
# The value is a CSSValueList.
CSS_VALUE_LIST = 2
# The value is a custom value.
CSS_CUSTOM = 3
# The value is a CSSVariable.
CSS_VARIABLE = 4
_typestrings = {
0: 'CSS_INHERIT',
1: 'CSS_PRIMITIVE_VALUE',
2: 'CSS_VALUE_LIST',
3: 'CSS_CUSTOM',
4: 'CSS_VARIABLE',
}
def __init__(self, cssText=None, parent=None, readonly=False):
"""
:param cssText:
the parsable cssText of the value
:param readonly:
defaults to False
"""
super().__init__()
self._cssValueType = None
self.wellformed = False
self.parent = parent
if cssText is not None: # may be 0
if isinstance(cssText, int):
cssText = str(cssText) # if it is an integer
elif isinstance(cssText, float):
cssText = '%f' % cssText # if it is a floating point number
self.cssText = cssText
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}({self.cssText!r})"
def __str__(self):
return (
""
% (self.__class__.__name__, self.cssValueTypeString, self.cssText, id(self))
)
def _setCssText(self, cssText): # noqa: C901
"""
Format::
unary_operator
: '-' | '+'
;
operator
: '/' S* | ',' S* | /* empty */
;
expr
: term [ operator term ]*
;
term
: unary_operator?
[ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
ANGLE S* | TIME S* | FREQ S* ]
| STRING S* | IDENT S* | URI S* | hexcolor | function
| UNICODE-RANGE S*
;
function
: FUNCTION S* expr ')' S*
;
/*
* There is a constraint on the color that it must
* have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
* after the "#"; e.g., "#000" is OK, but "#abcd" is not.
*/
hexcolor
: HASH S*
;
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
(according to the attached property) or is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
TODO: Raised if the specified CSS string value represents a
different type of values than the values allowed by the CSS
property.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this value is readonly.
"""
self._checkReadonly()
# used as operator is , / or S
nextSor = ',/'
term = Choice(
Sequence(
PreDef.unary(),
Choice(
PreDef.number(nextSor=nextSor),
PreDef.percentage(nextSor=nextSor),
PreDef.dimension(nextSor=nextSor),
),
),
PreDef.string(nextSor=nextSor),
PreDef.ident(nextSor=nextSor),
PreDef.uri(nextSor=nextSor),
PreDef.hexcolor(nextSor=nextSor),
PreDef.unicode_range(nextSor=nextSor),
# special case IE only expression
Prod(
name='expression',
match=lambda t, v: t == self._prods.FUNCTION
and (
cssutils.helper.normalize(v)
in (
'expression(',
'alpha(',
'blur(',
'chroma(',
'dropshadow(',
'fliph(',
'flipv(',
'glow(',
'gray(',
'invert(',
'mask(',
'shadow(',
'wave(',
'xray(',
)
or v.startswith('progid:DXImageTransform.Microsoft.')
),
nextSor=nextSor,
toSeq=lambda t, tokens: (
ExpressionValue._functionName,
ExpressionValue(cssutils.helper.pushtoken(t, tokens), parent=self),
),
),
# CSS Variable var(
PreDef.variable(
nextSor=nextSor,
toSeq=lambda t, tokens: (
'CSSVariable',
CSSVariable(cssutils.helper.pushtoken(t, tokens), parent=self),
),
),
# calc(
PreDef.calc(
nextSor=nextSor,
toSeq=lambda t, tokens: (
CalcValue._functionName,
CalcValue(cssutils.helper.pushtoken(t, tokens), parent=self),
),
),
# TODO:
# # rgb/rgba(
# Prod(name='RGBColor',
# match=lambda t, v: t == self._prods.FUNCTION and (
# cssutils.helper.normalize(v) in (u'rgb(',
# u'rgba('
# )
# ),
# nextSor=nextSor,
# toSeq=lambda t, tokens: (RGBColor._functionName,
# RGBColor(
# cssutils.helper.pushtoken(t, tokens),
# parent=self)
# )
# ),
# other functions like rgb( etc
PreDef.function(
nextSor=nextSor,
toSeq=lambda t, tokens: (
'FUNCTION',
CSSFunction(cssutils.helper.pushtoken(t, tokens), parent=self),
),
),
)
operator = Choice(
PreDef.S(),
PreDef.char('comma', ',', toSeq=lambda t, tokens: ('operator', t[1])),
PreDef.char('slash', '/', toSeq=lambda t, tokens: ('operator', t[1])),
optional=True,
)
# CSSValue PRODUCTIONS
valueprods = Sequence(
term,
Sequence(
operator, # mayEnd this Sequence if whitespace
# TODO: only when setting via other class
# used by variabledeclaration currently
PreDef.char('END', ';', stopAndKeep=True, optional=True),
term,
minmax=lambda: (0, None),
),
)
# parse
wellformed, seq, store, notused = ProdParser().parse(
cssText, 'CSSValue', valueprods, keepS=True
)
if wellformed:
# - count actual values and set firstvalue which is used later on
# - combine comma separated list, e.g. font-family to a single item
# - remove S which should be an operator but is no needed
count, firstvalue = 0, ()
newseq = self._tempSeq()
i, end = 0, len(seq)
while i < end:
item = seq[i]
if item.type == self._prods.S:
pass
elif (item.value, item.type) == (',', 'operator'):
# , separared counts as a single STRING for now
# URI or STRING value might be a single CHAR too!
newseq.appendItem(item)
count -= 1
if firstvalue:
# list of IDENTs is handled as STRING!
if firstvalue[1] == self._prods.IDENT:
firstvalue = firstvalue[0], 'STRING'
elif item.value == '/':
# / separated items count as one
newseq.appendItem(item)
elif item.value == '-' or item.value == '+':
# combine +- and following number or other
i += 1
try:
next = seq[i]
except IndexError:
firstvalue = () # raised later
break
newval = item.value + next.value
newseq.append(newval, next.type, item.line, item.col)
if not firstvalue:
firstvalue = (newval, next.type)
count += 1
elif item.type != cssutils.css.CSSComment:
newseq.appendItem(item)
if not firstvalue:
firstvalue = (item.value, item.type)
count += 1
else:
newseq.appendItem(item)
i += 1
if not firstvalue:
self._log.error(
'CSSValue: Unknown syntax or no value: %r.'
% self._valuestr(cssText)
)
else:
# ok and set
self._setSeq(newseq)
self.wellformed = wellformed
if hasattr(self, '_value'):
# only in case of CSSPrimitiveValue, else remove!
del self._value
if count == 1:
# inherit, primitive or variable
if isinstance(
firstvalue[0], str
) and 'inherit' == cssutils.helper.normalize(firstvalue[0]):
self.__class__ = CSSValue
self._cssValueType = CSSValue.CSS_INHERIT
elif 'CSSVariable' == firstvalue[1]:
self.__class__ = CSSVariable
self._value = firstvalue
# TODO: remove major hack!
self._name = firstvalue[0]._name
else:
self.__class__ = CSSPrimitiveValue
self._value = firstvalue
elif count > 1:
# valuelist
self.__class__ = CSSValueList
# change items in list to specific type (primitive etc)
newseq = self._tempSeq()
commalist = []
nexttocommalist = False
def itemValue(item):
"Reserialized simple item.value"
if self._prods.STRING == item.type:
return cssutils.helper.string(item.value)
elif self._prods.URI == item.type:
return cssutils.helper.uri(item.value)
elif (
self._prods.FUNCTION == item.type
or 'CSSVariable' == item.type
):
return item.value.cssText
else:
return item.value
def saveifcommalist(commalist, newseq):
"""
saves items in commalist to seq and items
if anything in there
"""
if commalist:
newseq.replace(
-1,
CSSPrimitiveValue(cssText=''.join(commalist)),
CSSPrimitiveValue,
newseq[-1].line,
newseq[-1].col,
)
del commalist[:]
for i, item in enumerate(self._seq):
if issubclass(type(item.value), CSSValue):
# set parent of CSSValueList items to the lists
# parent
item.value.parent = self.parent
if item.type in (
self._prods.DIMENSION,
self._prods.FUNCTION,
self._prods.HASH,
self._prods.IDENT,
self._prods.NUMBER,
self._prods.PERCENTAGE,
self._prods.STRING,
self._prods.URI,
self._prods.UNICODE_RANGE,
'CSSVariable',
):
if nexttocommalist:
# wait until complete
commalist.append(itemValue(item))
else:
saveifcommalist(commalist, newseq)
# append new item
if hasattr(item.value, 'cssText'):
newseq.append(
item.value,
item.value.__class__,
item.line,
item.col,
)
else:
newseq.append(
CSSPrimitiveValue(itemValue(item)),
CSSPrimitiveValue,
item.line,
item.col,
)
nexttocommalist = False
elif ',' == item.value:
if not commalist:
# save last item to commalist
commalist.append(itemValue(self._seq[i - 1]))
commalist.append(',')
nexttocommalist = True
else:
if nexttocommalist:
commalist.append(item.value.cssText)
else:
newseq.appendItem(item)
saveifcommalist(commalist, newseq)
self._setSeq(newseq)
else:
# should not happen...
self.__class__ = CSSValue
self._cssValueType = CSSValue.CSS_CUSTOM
cssText = property(
lambda self: cssutils.ser.do_css_CSSValue(self),
_setCssText,
doc="A string representation of the current value.",
)
cssValueType = property(
lambda self: self._cssValueType,
doc="A (readonly) code defining the type of the value.",
)
cssValueTypeString = property(
lambda self: CSSValue._typestrings.get(self.cssValueType, None),
doc="(readonly) Name of cssValueType.",
)
class CSSPrimitiveValue(CSSValue):
"""Represents a single CSS Value. May be used to determine the value of a
specific style property currently set in a block or to set a specific
style property explicitly within the block. Might be obtained from the
getPropertyCSSValue method of CSSStyleDeclaration.
Conversions are allowed between absolute values (from millimeters to
centimeters, from degrees to radians, and so on) but not between
relative values. (For example, a pixel value cannot be converted to a
centimeter value.) Percentage values can't be converted since they are
relative to the parent value (or another property value). There is one
exception for color percentage values: since a color percentage value
is relative to the range 0-255, a color percentage value can be
converted to a number; (see also the RGBColor interface).
"""
# constant: type of this CSSValue class
cssValueType = CSSValue.CSS_PRIMITIVE_VALUE
__types = cssutils.cssproductions.CSSProductions
# An integer indicating which type of unit applies to the value.
CSS_UNKNOWN = 0 # only obtainable via cssText
CSS_NUMBER = 1
CSS_PERCENTAGE = 2
CSS_EMS = 3
CSS_EXS = 4
CSS_PX = 5
CSS_CM = 6
CSS_MM = 7
CSS_IN = 8
CSS_PT = 9
CSS_PC = 10
CSS_DEG = 11
CSS_RAD = 12
CSS_GRAD = 13
CSS_MS = 14
CSS_S = 15
CSS_HZ = 16
CSS_KHZ = 17
CSS_DIMENSION = 18
CSS_STRING = 19
CSS_URI = 20
CSS_IDENT = 21
CSS_ATTR = 22
CSS_COUNTER = 23
CSS_RECT = 24
CSS_RGBCOLOR = 25
# NOT OFFICIAL:
CSS_RGBACOLOR = 26
CSS_UNICODE_RANGE = 27
_floattypes = (
CSS_NUMBER,
CSS_PERCENTAGE,
CSS_EMS,
CSS_EXS,
CSS_PX,
CSS_CM,
CSS_MM,
CSS_IN,
CSS_PT,
CSS_PC,
CSS_DEG,
CSS_RAD,
CSS_GRAD,
CSS_MS,
CSS_S,
CSS_HZ,
CSS_KHZ,
CSS_DIMENSION,
)
_stringtypes = (CSS_ATTR, CSS_IDENT, CSS_STRING, CSS_URI)
_countertypes = (CSS_COUNTER,)
_recttypes = (CSS_RECT,)
_rbgtypes = (CSS_RGBCOLOR, CSS_RGBACOLOR)
_lengthtypes = (
CSS_NUMBER,
CSS_EMS,
CSS_EXS,
CSS_PX,
CSS_CM,
CSS_MM,
CSS_IN,
CSS_PT,
CSS_PC,
)
# oldtype: newType: converterfunc
_converter = {
# cm <-> mm <-> in, 1 inch is equal to 2.54 centimeters.
# pt <-> pc, the points used by CSS 2.1 are equal to 1/72nd of an inch.
# pc: picas - 1 pica is equal to 12 points
(CSS_CM, CSS_MM): lambda x: x * 10,
(CSS_MM, CSS_CM): lambda x: x / 10,
(CSS_PT, CSS_PC): lambda x: x * 12,
(CSS_PC, CSS_PT): lambda x: x / 12,
(CSS_CM, CSS_IN): lambda x: x / 2.54,
(CSS_IN, CSS_CM): lambda x: x * 2.54,
(CSS_MM, CSS_IN): lambda x: x / 25.4,
(CSS_IN, CSS_MM): lambda x: x * 25.4,
(CSS_IN, CSS_PT): lambda x: x / 72,
(CSS_PT, CSS_IN): lambda x: x * 72,
(CSS_CM, CSS_PT): lambda x: x / 2.54 / 72,
(CSS_PT, CSS_CM): lambda x: x * 72 * 2.54,
(CSS_MM, CSS_PT): lambda x: x / 25.4 / 72,
(CSS_PT, CSS_MM): lambda x: x * 72 * 25.4,
(CSS_IN, CSS_PC): lambda x: x / 72 / 12,
(CSS_PC, CSS_IN): lambda x: x * 12 * 72,
(CSS_CM, CSS_PC): lambda x: x / 2.54 / 72 / 12,
(CSS_PC, CSS_CM): lambda x: x * 12 * 72 * 2.54,
(CSS_MM, CSS_PC): lambda x: x / 25.4 / 72 / 12,
(CSS_PC, CSS_MM): lambda x: x * 12 * 72 * 25.4,
# hz <-> khz
(CSS_KHZ, CSS_HZ): lambda x: x * 1000,
(CSS_HZ, CSS_KHZ): lambda x: x / 1000,
# s <-> ms
(CSS_S, CSS_MS): lambda x: x * 1000,
(CSS_MS, CSS_S): lambda x: x / 1000,
(CSS_RAD, CSS_DEG): lambda x: math.degrees(x),
(CSS_DEG, CSS_RAD): lambda x: math.radians(x),
# TODO: convert grad <-> deg or rad
# (CSS_RAD, CSS_GRAD): lambda x: math.degrees(x),
# (CSS_DEG, CSS_GRAD): lambda x: math.radians(x),
# (CSS_GRAD, CSS_RAD): lambda x: math.radians(x),
# (CSS_GRAD, CSS_DEG): lambda x: math.radians(x)
}
def __init__(self, cssText=None, parent=None, readonly=False):
"""See CSSPrimitiveValue.__init__()"""
super().__init__(cssText=cssText, parent=parent, readonly=readonly)
def __str__(self):
return f""
_unitnames = [
'CSS_UNKNOWN',
'CSS_NUMBER',
'CSS_PERCENTAGE',
'CSS_EMS',
'CSS_EXS',
'CSS_PX',
'CSS_CM',
'CSS_MM',
'CSS_IN',
'CSS_PT',
'CSS_PC',
'CSS_DEG',
'CSS_RAD',
'CSS_GRAD',
'CSS_MS',
'CSS_S',
'CSS_HZ',
'CSS_KHZ',
'CSS_DIMENSION',
'CSS_STRING',
'CSS_URI',
'CSS_IDENT',
'CSS_ATTR',
'CSS_COUNTER',
'CSS_RECT',
'CSS_RGBCOLOR',
'CSS_RGBACOLOR',
'CSS_UNICODE_RANGE',
]
_reNumDim = re.compile(r'([+-]?\d*\.\d+|[+-]?\d+)(.*)$', re.I | re.U | re.X)
def _unitDIMENSION(value):
"""Check val for dimension name."""
units = {
'em': 'CSS_EMS',
'ex': 'CSS_EXS',
'px': 'CSS_PX',
'cm': 'CSS_CM',
'mm': 'CSS_MM',
'in': 'CSS_IN',
'pt': 'CSS_PT',
'pc': 'CSS_PC',
'deg': 'CSS_DEG',
'rad': 'CSS_RAD',
'grad': 'CSS_GRAD',
'ms': 'CSS_MS',
's': 'CSS_S',
'hz': 'CSS_HZ',
'khz': 'CSS_KHZ',
}
val, dim = CSSPrimitiveValue._reNumDim.findall(
cssutils.helper.normalize(value)
)[0]
return units.get(dim, 'CSS_DIMENSION')
def _unitFUNCTION(value):
"""Check val for function name."""
units = {
'attr(': 'CSS_ATTR',
'counter(': 'CSS_COUNTER',
'rect(': 'CSS_RECT',
'rgb(': 'CSS_RGBCOLOR',
'rgba(': 'CSS_RGBACOLOR',
}
return units.get(
re.findall(r'^(.*?\()', cssutils.helper.normalize(value.cssText), re.U)[0],
'CSS_UNKNOWN',
)
__unitbytype = {
__types.NUMBER: 'CSS_NUMBER',
__types.PERCENTAGE: 'CSS_PERCENTAGE',
__types.STRING: 'CSS_STRING',
__types.UNICODE_RANGE: 'CSS_UNICODE_RANGE',
__types.URI: 'CSS_URI',
__types.IDENT: 'CSS_IDENT',
__types.HASH: 'CSS_RGBCOLOR',
__types.DIMENSION: _unitDIMENSION,
__types.FUNCTION: _unitFUNCTION,
}
def __set_primitiveType(self):
"""primitiveType is readonly but is set lazy if accessed"""
# TODO: check unary and font-family STRING a, b, "c"
val, type_ = self._value
# try get by type_
pt = self.__unitbytype.get(type_, 'CSS_UNKNOWN')
if callable(pt):
# multiple options, check value too
pt = pt(val)
self._primitiveType = getattr(self, pt)
def _getPrimitiveType(self):
if not hasattr(self, '_primitivetype'):
self.__set_primitiveType()
return self._primitiveType
primitiveType = property(
_getPrimitiveType,
doc="(readonly) The type of the value as defined "
"by the constants in this class.",
)
def _getPrimitiveTypeString(self):
return self._unitnames[self.primitiveType]
primitiveTypeString = property(
_getPrimitiveTypeString, doc="Name of primitive type of this value."
)
def _getCSSPrimitiveTypeString(self, type):
"get TypeString by given type which may be unknown, used by setters"
try:
return self._unitnames[type]
except (IndexError, TypeError):
return '%r (UNKNOWN TYPE)' % type
def _getNumDim(self, value=None):
"Split self._value in numerical and dimension part."
if value is None:
value = cssutils.helper.normalize(self._value[0])
try:
val, dim = CSSPrimitiveValue._reNumDim.findall(value)[0]
except IndexError:
val, dim = value, ''
try:
val = float(val)
if val == int(val):
val = int(val)
except ValueError as err:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: No float value %r' % self._value[0]
) from err
return val, dim
def getFloatValue(self, unitType=None):
"""(DOM) This method is used to get a float value in a
specified unit. If this CSS value doesn't contain a float value
or can't be converted into the specified unit, a DOMException
is raised.
:param unitType:
to get the float value. The unit code can only be a float unit type
(i.e. CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS, CSS_PX, CSS_CM,
CSS_MM, CSS_IN, CSS_PT, CSS_PC, CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS,
CSS_S, CSS_HZ, CSS_KHZ, CSS_DIMENSION) or None in which case
the current dimension is used.
:returns:
not necessarily a float but some cases just an integer
e.g. if the value is ``1px`` it return ``1`` and **not** ``1.0``
Conversions might return strange values like 1.000000000001
"""
if unitType is not None and unitType not in self._floattypes:
raise xml.dom.InvalidAccessErr('unitType Parameter is not a float type')
val, dim = self._getNumDim()
if unitType is not None and self.primitiveType != unitType:
# convert if needed
try:
val = self._converter[self.primitiveType, unitType](val)
except KeyError as err:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r'
% (
self.primitiveTypeString,
self._getCSSPrimitiveTypeString(unitType),
)
) from err
if val == int(val):
val = int(val)
return val
def setFloatValue(self, unitType, floatValue):
"""(DOM) A method to set the float value with a specified unit.
If the property attached with this value can not accept the
specified unit or the float value, the value will be unchanged and
a DOMException will be raised.
:param unitType:
a unit code as defined above. The unit code can only be a float
unit type
:param floatValue:
the new float value which does not have to be a float value but
may simple be an int e.g. if setting::
setFloatValue(CSS_PX, 1)
:exceptions:
- :exc:`~xml.dom.InvalidAccessErr`:
Raised if the attached property doesn't
support the float value or the unit type.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this property is readonly.
"""
self._checkReadonly()
if unitType not in self._floattypes:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: unitType %r is not a float type'
% self._getCSSPrimitiveTypeString(unitType)
)
try:
val = float(floatValue)
except ValueError as err:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: floatValue %r is not a float' % floatValue
) from err
oldval, dim = self._getNumDim()
if self.primitiveType != unitType:
# convert if possible
try:
val = self._converter[unitType, self.primitiveType](val)
except KeyError as err:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r'
% (
self.primitiveTypeString,
self._getCSSPrimitiveTypeString(unitType),
)
) from err
if val == int(val):
val = int(val)
self.cssText = f'{val}{dim}'
def getStringValue(self):
"""(DOM) This method is used to get the string value. If the
CSS value doesn't contain a string value, a DOMException is raised.
Some properties (like 'font-family' or 'voice-family')
convert a whitespace separated list of idents to a string.
Only the actual value is returned so e.g. all the following return the
actual value ``a``: url(a), attr(a), "a", 'a'
"""
if self.primitiveType not in self._stringtypes:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue %r is not a string type' % self.primitiveTypeString
)
if CSSPrimitiveValue.CSS_ATTR == self.primitiveType:
return self._value[0].cssText[5:-1]
else:
return self._value[0]
def setStringValue(self, stringType, stringValue):
"""(DOM) A method to set the string value with the specified
unit. If the property attached to this value can't accept the
specified unit or the string value, the value will be unchanged and
a DOMException will be raised.
:param stringType:
a string code as defined above. The string code can only be a
string unit type (i.e. CSS_STRING, CSS_URI, CSS_IDENT, and
CSS_ATTR).
:param stringValue:
the new string value
Only the actual value is expected so for (CSS_URI, "a") the
new value will be ``url(a)``. For (CSS_STRING, "'a'")
the new value will be ``"\\'a\\'"`` as the surrounding ``'`` are
not part of the string value
:exceptions:
- :exc:`~xml.dom.InvalidAccessErr`:
Raised if the CSS value doesn't contain a
string value or if the string value can't be converted into
the specified unit.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this property is readonly.
"""
self._checkReadonly()
# self not stringType
if self.primitiveType not in self._stringtypes:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue %r is not a string type' % self.primitiveTypeString
)
# given stringType is no StringType
if stringType not in self._stringtypes:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: stringType %s is not a string type'
% self._getCSSPrimitiveTypeString(stringType)
)
if self._primitiveType != stringType:
raise xml.dom.InvalidAccessErr(
'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r'
% (
self.primitiveTypeString,
self._getCSSPrimitiveTypeString(stringType),
)
)
if CSSPrimitiveValue.CSS_STRING == self._primitiveType:
self.cssText = cssutils.helper.string(stringValue)
elif CSSPrimitiveValue.CSS_URI == self._primitiveType:
self.cssText = cssutils.helper.uri(stringValue)
elif CSSPrimitiveValue.CSS_ATTR == self._primitiveType:
self.cssText = 'attr(%s)' % stringValue
else:
self.cssText = stringValue
self._primitiveType = stringType
def getCounterValue(self):
"""(DOM) This method is used to get the Counter value. If
this CSS value doesn't contain a counter value, a DOMException
is raised. Modification to the corresponding style property
can be achieved using the Counter interface.
**Not implemented.**
"""
if not self.CSS_COUNTER == self.primitiveType:
raise xml.dom.InvalidAccessErr('Value is not a counter type')
# TODO: use Counter class
raise NotImplementedError()
def getRGBColorValue(self):
"""(DOM) This method is used to get the RGB color. If this
CSS value doesn't contain a RGB color value, a DOMException
is raised. Modification to the corresponding style property
can be achieved using the RGBColor interface.
"""
if self.primitiveType not in self._rbgtypes:
raise xml.dom.InvalidAccessErr('Value is not a RGBColor value')
return RGBColor(self._value[0])
def getRectValue(self):
"""(DOM) This method is used to get the Rect value. If this CSS
value doesn't contain a rect value, a DOMException is raised.
Modification to the corresponding style property can be achieved
using the Rect interface.
**Not implemented.**
"""
if self.primitiveType not in self._recttypes:
raise xml.dom.InvalidAccessErr('value is not a Rect value')
# TODO: use Rect class
raise NotImplementedError()
def _getCssText(self):
"""Overwrites CSSValue."""
return cssutils.ser.do_css_CSSPrimitiveValue(self)
def _setCssText(self, cssText):
"""Use CSSValue."""
return super()._setCssText(cssText)
cssText = property(
_getCssText, _setCssText, doc="A string representation of the current value."
)
class CSSValueList(CSSValue):
"""The CSSValueList interface provides the abstraction of an ordered
collection of CSS values.
Some properties allow an empty list into their syntax. In that case,
these properties take the none identifier. So, an empty list means
that the property has the value none.
The items in the CSSValueList are accessible via an integral index,
starting from 0.
"""
cssValueType = CSSValue.CSS_VALUE_LIST
def __init__(self, cssText=None, parent=None, readonly=False):
"""Init a new CSSValueList"""
super().__init__(cssText=cssText, parent=parent, readonly=readonly)
self._items = []
def __iter__(self):
"CSSValueList is iterable."
for item in self.__items():
yield item.value
def __str__(self):
return (
""
% (
self.__class__.__name__,
self.cssValueTypeString,
self.cssText,
self.length,
id(self),
)
)
def __items(self):
return [item for item in self._seq if isinstance(item.value, CSSValue)]
def item(self, index):
"""(DOM) Retrieve a CSSValue by ordinal `index`. The
order in this collection represents the order of the values in the
CSS style property. If `index` is greater than or equal to the number
of values in the list, this returns ``None``.
"""
try:
return self.__items()[index].value
except IndexError:
return None
length = property(
lambda self: len(self.__items()),
doc="(DOM attribute) The number of CSSValues in the " "list.",
)
class CSSFunction(CSSPrimitiveValue):
"""A CSS function value like rect() etc."""
_functionName = 'CSSFunction'
primitiveType = CSSPrimitiveValue.CSS_UNKNOWN
def __init__(self, cssText=None, parent=None, readonly=False):
"""
Init a new CSSFunction
:param cssText:
the parsable cssText of the value
:param readonly:
defaults to False
"""
super().__init__(parent=parent)
self._funcType = None
self.valid = False
self.wellformed = False
if cssText is not None:
self.cssText = cssText
self._readonly = readonly
def _productiondefinition(self):
"""Return definition used for parsing."""
types = self._prods # rename!
value = Sequence(
PreDef.unary(),
Prod(
name='PrimitiveValue',
match=lambda t, v: t
in (
types.DIMENSION,
types.HASH,
types.IDENT,
types.NUMBER,
types.PERCENTAGE,
types.STRING,
),
toSeq=lambda t, tokens: (t[0], CSSPrimitiveValue(t[1])),
),
)
valueOrFunc = Choice(
value,
# FUNC is actually not in spec but used in e.g. Prince
PreDef.function(
toSeq=lambda t, tokens: (
'FUNCTION',
CSSFunction(cssutils.helper.pushtoken(t, tokens)),
)
),
)
funcProds = Sequence(
Prod(
name='FUNC',
match=lambda t, v: t == types.FUNCTION,
toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1])),
),
Choice(
Sequence(
valueOrFunc,
# more values starting with Comma
# should use store where colorType is saved to
# define min and may, closure?
Sequence(PreDef.comma(), valueOrFunc, minmax=lambda: (0, None)),
PreDef.funcEnd(stop=True),
),
PreDef.funcEnd(stop=True),
),
)
return funcProds
def _setCssText(self, cssText):
self._checkReadonly()
# store: colorType, parts
wellformed, seq, store, unusedtokens = ProdParser().parse(
cssText, self._functionName, self._productiondefinition(), keepS=True
)
if wellformed:
# combine +/- and following CSSPrimitiveValue, remove S
newseq = self._tempSeq()
i, end = 0, len(seq)
while i < end:
item = seq[i]
if item.type == self._prods.S:
pass
elif item.value == '+' or item.value == '-':
i += 1
next = seq[i]
newval = next.value
if isinstance(newval, CSSPrimitiveValue):
newval.setFloatValue(
newval.primitiveType,
float(item.value + str(newval.getFloatValue())),
)
newseq.append(newval, next.type, item.line, item.col)
else:
# expressions only?
newseq.appendItem(item)
newseq.appendItem(next)
else:
newseq.appendItem(item)
i += 1
self.wellformed = True
self._setSeq(newseq)
self._funcType = newseq[0].value
cssText = property(
lambda self: cssutils.ser.do_css_FunctionValue(self), _setCssText
)
funcType = property(lambda self: self._funcType)
class RGBColor(CSSFunction):
"""A CSS color like RGB, RGBA or a simple value like `#000` or `red`."""
_functionName = 'Function rgb()'
def __init__(self, cssText=None, parent=None, readonly=False):
"""
Init a new RGBColor
:param cssText:
the parsable cssText of the value
:param readonly:
defaults to False
"""
super(CSSFunction, self).__init__(parent=parent)
self._colorType = None
self.valid = False
self.wellformed = False
if cssText is not None:
try:
# if it is a Function object
cssText = cssText.cssText
except AttributeError:
pass
self.cssText = cssText
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}({self.cssText!r})"
def __str__(self):
return f""
def _setCssText(self, cssText):
self._checkReadonly()
types = self._prods # rename!
valueProd = Prod(
name='value',
match=lambda t, v: t in (types.NUMBER, types.PERCENTAGE),
toSeq=lambda t, v: (CSSPrimitiveValue, CSSPrimitiveValue(v)),
toStore='parts',
)
# COLOR PRODUCTION
funccolor = Sequence(
Prod(
name='FUNC',
match=lambda t, v: t == types.FUNCTION
and cssutils.helper.normalize(v) in ('rgb(', 'rgba(', 'hsl(', 'hsla('),
toSeq=lambda t, v: (t, v), # cssutils.helper.normalize(v)),
toStore='colorType',
),
PreDef.unary(),
valueProd,
# 2 or 3 more values starting with Comma
Sequence(PreDef.comma(), PreDef.unary(), valueProd, minmax=lambda: (2, 3)),
PreDef.funcEnd(),
)
colorprods = Choice(
funccolor,
PreDef.hexcolor('colorType'),
Prod(
name='named color',
match=lambda t, v: t == types.IDENT,
toStore='colorType',
),
)
# store: colorType, parts
wellformed, seq, store, unusedtokens = ProdParser().parse(
cssText, 'RGBColor', colorprods, keepS=True, store={'parts': []}
)
if wellformed:
self.wellformed = True
if store['colorType'].type == self._prods.HASH:
self._colorType = 'HEX'
elif store['colorType'].type == self._prods.IDENT:
self._colorType = 'Named Color'
else:
self._colorType = store['colorType'].value[:-1]
# self._colorType = \
# cssutils.helper.normalize(store['colorType'].value)[:-1]
self._setSeq(seq)
cssText = property(lambda self: cssutils.ser.do_css_RGBColor(self), _setCssText)
colorType = property(lambda self: self._colorType)
class CalcValue(CSSFunction):
"""Calc Function"""
_functionName = 'Function calc()'
def _productiondefinition(self):
"""Return defintion used for parsing."""
types = self._prods # rename!
def toSeq(t, tokens):
"Do not normalize function name!"
return t[0], t[1]
funcProds = Sequence(
Prod(name='calc', match=lambda t, v: t == types.FUNCTION, toSeq=toSeq),
Sequence(
Choice(
Prod(
name='nested function',
match=lambda t, v: t == self._prods.FUNCTION,
toSeq=lambda t, tokens: (
CSSFunction._functionName,
CSSFunction(cssutils.helper.pushtoken(t, tokens)),
),
),
Prod(
name='part',
match=lambda t, v: v != ')',
toSeq=lambda t, tokens: (t[0], t[1]),
),
),
minmax=lambda: (0, None),
),
PreDef.funcEnd(stop=True),
)
return funcProds
def _getCssText(self):
return cssutils.ser.do_css_CalcValue(self)
def _setCssText(self, cssText):
return super()._setCssText(cssText)
cssText = property(
_getCssText, _setCssText, doc="A string representation of the current value."
)
class ExpressionValue(CSSFunction):
"""Special IE only CSSFunction which may contain *anything*.
Used for expressions and ``alpha(opacity=100)`` currently."""
_functionName = 'Expression (IE only)'
def _productiondefinition(self):
"""Return defintion used for parsing."""
types = self._prods # rename!
def toSeq(t, tokens):
"Do not normalize function name!"
return t[0], t[1]
funcProds = Sequence(
Prod(
name='expression', match=lambda t, v: t == types.FUNCTION, toSeq=toSeq
),
Sequence(
Choice(
Prod(
name='nested function',
match=lambda t, v: t == self._prods.FUNCTION,
toSeq=lambda t, tokens: (
ExpressionValue._functionName,
ExpressionValue(cssutils.helper.pushtoken(t, tokens)),
),
),
Prod(
name='part',
match=lambda t, v: v != ')',
toSeq=lambda t, tokens: (t[0], t[1]),
),
),
minmax=lambda: (0, None),
),
PreDef.funcEnd(stop=True),
)
return funcProds
def _getCssText(self):
return cssutils.ser.do_css_ExpressionValue(self)
def _setCssText(self, cssText):
# self._log.warn(u'CSSValue: Unoffial and probably invalid MS value used!')
return super()._setCssText(cssText)
cssText = property(
_getCssText, _setCssText, doc="A string representation of the current value."
)
class CSSVariable(CSSValue):
"""The CSSVariable represents a call to CSS Variable."""
def __init__(self, cssText=None, parent=None, readonly=False):
"""Init a new CSSVariable.
:param cssText:
the parsable cssText of the value, e.g. ``var(x)``
:param readonly:
defaults to False
"""
self._name = None
super().__init__(cssText=cssText, parent=parent, readonly=readonly)
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}({self.cssText!r})"
def __str__(self):
return f""
def _setCssText(self, cssText):
self._checkReadonly()
types = self._prods # rename!
funcProds = Sequence(
Prod(name='var', match=lambda t, v: t == types.FUNCTION),
PreDef.ident(toStore='ident'),
PreDef.funcEnd(stop=True),
)
# store: name of variable
store = {'ident': None}
wellformed, seq, store, unusedtokens = ProdParser().parse(
cssText, 'CSSVariable', funcProds, keepS=True
)
if wellformed:
self._name = store['ident'].value
self._setSeq(seq)
self.wellformed = True
cssText = property(
lambda self: cssutils.ser.do_css_CSSVariable(self),
_setCssText,
doc="A string representation of the current variable.",
)
cssValueType = CSSValue.CSS_VARIABLE
# TODO: writable? check if var (value) available?
name = property(lambda self: self._name)
def _getValue(self):
"Find contained sheet and @variables there"
try:
variables = self.parent.parent.parentRule.parentStyleSheet.variables
except AttributeError:
return None
else:
try:
return variables[self.name]
except KeyError:
return None
value = property(_getValue)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssvariablesdeclaration.py 0000644 0001751 0000177 00000026260 14627633762 023002 0 ustar 00runner docker """CSSVariablesDeclaration
http://disruptive-innovations.com/zoo/cssvariables/#mozTocId496530
"""
__all__ = ['CSSVariablesDeclaration']
import itertools
import cssutils
from cssutils.helper import normalize
from cssutils.prodparser import PreDef, Prod, ProdParser, Sequence
from .value import PropertyValue
class CSSVariablesDeclaration(cssutils.util._NewBase):
"""The CSSVariablesDeclaration interface represents a single block of
variable declarations.
"""
def __init__(self, cssText='', parentRule=None, readonly=False):
"""
:param cssText:
Shortcut, sets CSSVariablesDeclaration.cssText
:param parentRule:
The CSS rule that contains this declaration block or
None if this CSSVariablesDeclaration is not attached to a CSSRule.
:param readonly:
defaults to False
Format::
variableset
: vardeclaration [ ';' S* vardeclaration ]* S*
;
vardeclaration
: varname ':' S* term
;
varname
: IDENT S*
;
"""
super().__init__()
self._parentRule = parentRule
self._vars = {}
if cssText:
self.cssText = cssText
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(cssText={self.cssText!r})"
def __str__(self):
return f""
def __contains__(self, variableName):
"""Check if a variable is in variable declaration block.
:param variableName:
a string
"""
return normalize(variableName) in list(self.keys())
def __getitem__(self, variableName):
"""Retrieve the value of variable ``variableName`` from this
declaration.
"""
return self.getVariableValue(variableName)
def __setitem__(self, variableName, value):
self.setVariable(variableName, value)
def __delitem__(self, variableName):
return self.removeVariable(variableName)
def __iter__(self):
"""Iterator of names of set variables."""
yield from list(self.keys())
def keys(self):
"""Analoguous to standard dict returns variable names which are set in
this declaration."""
return list(self._vars.keys())
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_css_CSSVariablesDeclaration(self)
def _setCssText(self, cssText):
"""Setting this attribute will result in the parsing of the new value
and resetting of all the properties in the declaration block
including the removal or addition of properties.
:exceptions:
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this declaration is readonly or a property is readonly.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
Format::
variableset
: vardeclaration [ ';' S* vardeclaration ]*
;
vardeclaration
: varname ':' S* term
;
varname
: IDENT S*
;
expr
: [ VARCALL | term ] [ operator [ VARCALL | term ] ]*
;
"""
self._checkReadonly()
vardeclaration = Sequence(
PreDef.ident(),
PreDef.char(':', ':', toSeq=False, optional=True),
# PreDef.S(toSeq=False, optional=True),
Prod(
name='term',
match=lambda t, v: True,
toSeq=lambda t, tokens: (
'value',
PropertyValue(itertools.chain([t], tokens), parent=self),
),
),
)
prods = Sequence(
vardeclaration,
Sequence(
PreDef.S(optional=True),
PreDef.char(';', ';', toSeq=False, optional=True),
PreDef.S(optional=True),
vardeclaration,
minmax=lambda: (0, None),
),
PreDef.S(optional=True),
PreDef.char(';', ';', toSeq=False, optional=True),
)
# parse
wellformed, seq, store, notused = ProdParser().parse(
cssText, 'CSSVariableDeclaration', prods, emptyOk=True
)
if wellformed:
newseq = self._tempSeq()
newvars = {}
# seq contains only name: value pairs plus comments etc
nameitem = None
for item in seq:
if 'IDENT' == item.type:
nameitem = item
elif 'value' == item.type:
nname = normalize(nameitem.value)
if nname in newvars:
# replace var with same name
for i, it in enumerate(newseq):
if normalize(it.value[0]) == nname:
newseq.replace(
i,
(nameitem.value, item.value),
'var',
nameitem.line,
nameitem.col,
)
else:
# saved non normalized name for reserialization
newseq.append(
(nameitem.value, item.value),
'var',
nameitem.line,
nameitem.col,
)
# newseq.append((nameitem.value, item.value),
# 'var',
# nameitem.line, nameitem.col)
newvars[nname] = item.value
else:
newseq.appendItem(item)
self._setSeq(newseq)
self._vars = newvars
self.wellformed = True
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) A parsable textual representation of the declaration "
"block excluding the surrounding curly braces.",
)
def _setParentRule(self, parentRule):
self._parentRule = parentRule
parentRule = property(
lambda self: self._parentRule,
_setParentRule,
doc="(DOM) The CSS rule that contains this"
" declaration block or None if this block"
" is not attached to a CSSRule.",
)
def getVariableValue(self, variableName):
"""Used to retrieve the value of a variable if it has been explicitly
set within this variable declaration block.
:param variableName:
The name of the variable.
:returns:
the value of the variable if it has been explicitly set in this
variable declaration block. Returns the empty string if the
variable has not been set.
"""
try:
return self._vars[normalize(variableName)].cssText
except KeyError:
return ''
def removeVariable(self, variableName):
"""Used to remove a variable if it has been explicitly set within this
variable declaration block.
:param variableName:
The name of the variable.
:returns:
the value of the variable if it has been explicitly set for this
variable declaration block. Returns the empty string if the
variable has not been set.
:exceptions:
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this declaration is readonly is readonly.
"""
normalname = variableName
try:
r = self._vars[normalname]
except KeyError:
return ''
else:
self.seq._readonly = False
if normalname in self._vars:
for i, x in enumerate(self.seq):
if x.value[0] == variableName:
del self.seq[i]
self.seq._readonly = True
del self._vars[normalname]
return r.cssText
def setVariable(self, variableName, value):
"""Used to set a variable value within this variable declaration block.
:param variableName:
The name of the CSS variable.
:param value:
The new value of the variable, may also be a PropertyValue object.
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified value has a syntax error and is
unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this declaration is readonly or the property is
readonly.
"""
self._checkReadonly()
# check name
wellformed, seq, store, unused = ProdParser().parse(
normalize(variableName), 'variableName', Sequence(PreDef.ident())
)
if not wellformed:
self._log.error(f'Invalid variableName: {variableName!r}: {value!r}')
else:
# check value
if isinstance(value, PropertyValue):
v = value
else:
v = PropertyValue(cssText=value, parent=self)
if not v.wellformed:
self._log.error(f'Invalid variable value: {variableName!r}: {value!r}')
else:
# update seq
self.seq._readonly = False
variableName = normalize(variableName)
if variableName in self._vars:
for i, x in enumerate(self.seq):
if x.value[0] == variableName:
self.seq.replace(
i, [variableName, v], x.type, x.line, x.col
)
break
else:
self.seq.append([variableName, v], 'var')
self.seq._readonly = True
self._vars[variableName] = v
def item(self, index):
"""Used to retrieve the variables that have been explicitly set in
this variable declaration block. The order of the variables
retrieved using this method does not have to be the order in which
they were set. This method can be used to iterate over all variables
in this variable declaration block.
:param index:
of the variable name to retrieve, negative values behave like
negative indexes on Python lists, so -1 is the last element
:returns:
The name of the variable at this ordinal position. The empty
string if no variable exists at this position.
"""
try:
return list(self.keys())[index]
except IndexError:
return ''
length = property(
lambda self: len(self._vars),
doc="The number of variables that have been explicitly set in this"
" variable declaration block. The range of valid indices is 0"
" to length-1 inclusive.",
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/cssvariablesrule.py 0000644 0001751 0000177 00000015723 14627633762 021466 0 ustar 00runner docker """CSSVariables implements (and only partly) experimental
`CSS Variables `_
"""
__all__ = ['CSSVariablesRule']
import xml.dom
import cssutils
from . import cssrule
from .cssvariablesdeclaration import CSSVariablesDeclaration
class CSSVariablesRule(cssrule.CSSRule):
"""
The CSSVariablesRule interface represents a @variables rule within a CSS
style sheet. The @variables rule is used to specify variables.
cssutils uses a :class:`~cssutils.css.CSSVariablesDeclaration` to
represent the variables.
Format::
variables
VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S*
variableset* '}' S*
;
for variableset see :class:`cssutils.css.CSSVariablesDeclaration`
**Media are not implemented. Reason is that cssutils is using CSS
variables in a kind of preprocessing and therefor no media information
is available at this stage. For now do not use media!**
Example::
@variables {
CorporateLogoBGColor: #fe8d12;
}
div.logoContainer {
background-color: var(CorporateLogoBGColor);
}
"""
def __init__(
self,
mediaText=None,
variables=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""
If readonly allows setting of properties in constructor only.
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = '@variables'
# dummy
self._media = cssutils.stylesheets.MediaList(mediaText, readonly=readonly)
if variables:
self.variables = variables
else:
self.variables = CSSVariablesDeclaration(parentRule=self)
self._readonly = readonly
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(mediaText={self._media.mediaText!r}, variables={self.variables.cssText!r})"
def __str__(self):
return (
""
% (
self.__class__.__name__,
self._media.mediaText,
self.variables.cssText,
self.valid,
id(self),
)
)
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_CSSVariablesRule(self)
def _setCssText(self, cssText):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
Format::
variables
: VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S*
variableset* '}' S*
;
variableset
: LBRACE S* vardeclaration [ ';' S* vardeclaration ]* '}' S*
;
"""
super()._setCssText(cssText)
tokenizer = self._tokenize2(cssText)
attoken = self._nexttoken(tokenizer, None)
if self._type(attoken) != self._prods.VARIABLES_SYM:
self._log.error(
'CSSVariablesRule: No CSSVariablesRule found: %s'
% self._valuestr(cssText),
error=xml.dom.InvalidModificationErr,
)
else:
newVariables = CSSVariablesDeclaration(parentRule=self)
ok = True
beforetokens, brace = self._tokensupto2(
tokenizer, blockstartonly=True, separateEnd=True
)
if self._tokenvalue(brace) != '{':
ok = False
self._log.error(
'CSSVariablesRule: No start { of variable '
'declaration found: %r' % self._valuestr(cssText),
brace,
)
# parse stuff before { which should be comments and S only
new = {'wellformed': True}
newseq = self._tempSeq() # []
beforewellformed, expected = self._parse(
expected=':',
seq=newseq,
tokenizer=self._tokenize2(beforetokens),
productions={},
)
ok = ok and beforewellformed and new['wellformed']
variablestokens, braceorEOFtoken = self._tokensupto2(
tokenizer, blockendonly=True, separateEnd=True
)
val, type_ = self._tokenvalue(braceorEOFtoken), self._type(braceorEOFtoken)
if val != '}' and type_ != 'EOF':
ok = False
self._log.error(
'CSSVariablesRule: No "}" after variables '
'declaration found: %r' % self._valuestr(cssText)
)
nonetoken = self._nexttoken(tokenizer)
if nonetoken:
ok = False
self._log.error(
'CSSVariablesRule: Trailing content found.', token=nonetoken
)
if 'EOF' == type_:
# add again as variables needs it
variablestokens.append(braceorEOFtoken)
# SET but may raise:
newVariables.cssText = variablestokens
if ok:
# contains probably comments only upto {
self._setSeq(newseq)
self.variables = newVariables
cssText = property(
_getCssText,
_setCssText,
doc="(DOM) The parsable textual representation of this " "rule.",
)
media = property(
doc="NOT IMPLEMENTED! As cssutils resolves variables "
"during serializing media information is lost."
)
def _setVariables(self, variables):
"""
:param variables:
a CSSVariablesDeclaration or string
"""
self._checkReadonly()
if isinstance(variables, str):
self._variables = CSSVariablesDeclaration(
cssText=variables, parentRule=self
)
else:
variables._parentRule = self
self._variables = variables
variables = property(
lambda self: self._variables,
_setVariables,
doc="(DOM) The variables of this rule set, a "
":class:`cssutils.css.CSSVariablesDeclaration`.",
)
type = property(
lambda self: self.VARIABLES_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
valid = property(lambda self: True, doc='NOT IMPLEMTED REALLY (TODO)')
# constant but needed:
wellformed = property(lambda self: True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/marginrule.py 0000644 0001751 0000177 00000015176 14627633762 020264 0 ustar 00runner docker """MarginRule implements DOM Level 2 CSS MarginRule."""
__all__ = ['MarginRule']
import xml.dom
import cssutils
from cssutils.prodparser import Choice, PreDef, Prod, ProdParser, Sequence
from . import cssrule
from .cssstyledeclaration import CSSStyleDeclaration
class MarginRule(cssrule.CSSRule):
"""
A margin at-rule consists of an ATKEYWORD that identifies the margin box
(e.g. '@top-left') and a block of declarations (said to be in the margin
context).
Format::
margin :
margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
;
margin_sym :
TOPLEFTCORNER_SYM |
TOPLEFT_SYM |
TOPCENTER_SYM |
TOPRIGHT_SYM |
TOPRIGHTCORNER_SYM |
BOTTOMLEFTCORNER_SYM |
BOTTOMLEFT_SYM |
BOTTOMCENTER_SYM |
BOTTOMRIGHT_SYM |
BOTTOMRIGHTCORNER_SYM |
LEFTTOP_SYM |
LEFTMIDDLE_SYM |
LEFTBOTTOM_SYM |
RIGHTTOP_SYM |
RIGHTMIDDLE_SYM |
RIGHTBOTTOM_SYM
;
e.g.::
@top-left {
content: "123";
}
"""
margins = [
'@top-left-corner',
'@top-left',
'@top-center',
'@top-right',
'@top-right-corner',
'@bottom-left-corner',
'@bottom-left',
'@bottom-center',
'@bottom-right',
'@bottom-right-corner',
'@left-top',
'@left-middle',
'@left-bottom',
'@right-top',
'@right-middle',
'@right-bottom',
]
def __init__(
self,
margin=None,
style=None,
parentRule=None,
parentStyleSheet=None,
readonly=False,
):
"""
:param atkeyword:
The margin area, e.g. '@top-left' for this rule
:param style:
CSSStyleDeclaration for this MarginRule
"""
super().__init__(parentRule=parentRule, parentStyleSheet=parentStyleSheet)
self._atkeyword = self._keyword = None
if margin:
self.margin = margin
if style:
self.style = style
else:
self.style = CSSStyleDeclaration(parentRule=self)
self._readonly = readonly
def _setMargin(self, margin):
"""Check if new keyword fits the rule it is used for."""
n = self._normalize(margin)
if n not in MarginRule.margins:
self._log.error(
f'Invalid margin @keyword for this {self.margin} rule: {margin!r}',
error=xml.dom.InvalidModificationErr,
)
else:
self._atkeyword = n
self._keyword = margin
margin = property(
lambda self: self._atkeyword,
_setMargin,
doc="Margin area of parent CSSPageRule. "
"`margin` and `atkeyword` are both normalized "
"@keyword of the @rule.",
)
atkeyword = margin
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(margin={self.margin!r}, style={self.style.cssText!r})"
def __str__(self):
return "" % (
self.__class__.__name__,
self.margin,
self.style.cssText,
id(self),
)
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_MarginRule(self)
def _setCssText(self, cssText):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
Raised if the specified CSS string value represents a different
type of rule than the current one.
- :exc:`~xml.dom.HierarchyRequestErr`:
Raised if the rule cannot be inserted at this point in the
style sheet.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
super()._setCssText(cssText)
# TEMP: all style tokens are saved in store to fill styledeclaration
# TODO: resolve when all generators
styletokens = Prod(
name='styletokens',
match=lambda t, v: v != '}',
# toSeq=False,
toStore='styletokens',
storeToken=True,
)
prods = Sequence(
Prod(
name='@ margin',
match=lambda t, v: t == 'ATKEYWORD'
and self._normalize(v) in MarginRule.margins,
toStore='margin',
# TODO?
# , exception=xml.dom.InvalidModificationErr
),
PreDef.char('OPEN', '{'),
Sequence(
Choice(PreDef.unknownrule(toStore='@'), styletokens),
minmax=lambda: (0, None),
),
PreDef.char('CLOSE', '}', stopAndKeep=True),
)
# parse
ok, seq, store, unused = ProdParser().parse(cssText, 'MarginRule', prods)
if ok:
# TODO: use seq for serializing instead of fixed stuff?
self._setSeq(seq)
if 'margin' in store:
# may raise:
self.margin = store['margin'].value
else:
self._log.error(
'No margin @keyword for this %s rule' % self.margin,
error=xml.dom.InvalidModificationErr,
)
# new empty style
self.style = CSSStyleDeclaration(parentRule=self)
if 'styletokens' in store:
# may raise:
self.style.cssText = store['styletokens']
cssText = property(
fget=_getCssText,
fset=_setCssText,
doc="(DOM) The parsable textual representation.",
)
def _setStyle(self, style):
"""
:param style: A string or CSSStyleDeclaration which replaces the
current style object.
"""
self._checkReadonly()
if isinstance(style, str):
self._style = CSSStyleDeclaration(cssText=style, parentRule=self)
else:
style._parentRule = self
self._style = style
style = property(
lambda self: self._style,
_setStyle,
doc="(DOM) The declaration-block of this rule set.",
)
type = property(
lambda self: self.MARGIN_RULE,
doc="The type of this rule, as defined by a CSSRule " "type constant.",
)
wellformed = property(lambda self: bool(self.atkeyword))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/property.py 0000644 0001751 0000177 00000043421 14627633762 017775 0 ustar 00runner docker """Property is a single CSS property in a CSSStyleDeclaration."""
__all__ = ['Property']
import cssutils
from cssutils.helper import Deprecated
from .value import PropertyValue
class Property(cssutils.util.Base):
"""A CSS property in a StyleDeclaration of a CSSStyleRule (cssutils).
Format::
property = name
: IDENT S*
;
expr = value
: term [ operator term ]*
;
term
: unary_operator?
[ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
ANGLE S* | TIME S* | FREQ S* | function ]
| STRING S* | IDENT S* | URI S* | hexcolor
;
function
: FUNCTION S* expr ')' S*
;
/*
* There is a constraint on the color that it must
* have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
* after the "#"; e.g., "#000" is OK, but "#abcd" is not.
*/
hexcolor
: HASH S*
;
prio
: IMPORTANT_SYM S*
;
"""
def __init__(
self, name=None, value=None, priority='', _mediaQuery=False, parent=None
):
"""
:param name:
a property name string (will be normalized)
:param value:
a property value string
:param priority:
an optional priority string which currently must be u'',
u'!important' or u'important'
:param _mediaQuery:
if ``True`` value is optional (used by MediaQuery)
:param parent:
the parent object, normally a
:class:`cssutils.css.CSSStyleDeclaration`
"""
super().__init__()
self.seqs = [[], None, []]
self.wellformed = False
self._mediaQuery = _mediaQuery
self.parent = parent
self.__nametoken = None
self._name = ''
self._literalname = ''
self.seqs[1] = PropertyValue(parent=self)
if name:
self.name = name
self.propertyValue = value
self._priority = ''
self._literalpriority = ''
if priority:
self.priority = priority
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}(name={self.literalname!r}, value={self.propertyValue.cssText!r}, priority={self.priority!r})"
def __str__(self):
return f"<{self.__class__.__module__}.{self.__class__.__name__} object name={self.name!r} value={self.propertyValue.cssText!r} priority={self.priority!r} valid={self.valid!r} at 0x{id(self):x}>"
def _isValidating(self):
"""Return True if validation is enabled."""
try:
return self.parent.validating
except AttributeError:
# default (no parent)
return True
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_Property(self)
def _setCssText(self, cssText):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
# check and prepare tokenlists for setting
tokenizer = self._tokenize2(cssText)
nametokens = self._tokensupto2(tokenizer, propertynameendonly=True)
if nametokens:
wellformed = True
valuetokens = self._tokensupto2(tokenizer, propertyvalueendonly=True)
prioritytokens = self._tokensupto2(tokenizer, propertypriorityendonly=True)
if self._mediaQuery and not valuetokens:
# MediaQuery may consist of name only
self.name = nametokens
self.propertyValue = None
self.priority = None
return
# remove colon from nametokens
colontoken = nametokens.pop()
if self._tokenvalue(colontoken) != ':':
wellformed = False
self._log.error(
'Property: No ":" after name found: %s' % self._valuestr(cssText),
colontoken,
)
elif not nametokens:
wellformed = False
self._log.error(
'Property: No property name found: %s' % self._valuestr(cssText),
colontoken,
)
if valuetokens:
if self._tokenvalue(valuetokens[-1]) == '!':
# priority given, move "!" to prioritytokens
prioritytokens.insert(0, valuetokens.pop(-1))
else:
wellformed = False
self._log.error(
'Property: No property value found: %s' % self._valuestr(cssText),
colontoken,
)
if wellformed:
self.wellformed = True
self.name = nametokens
self.propertyValue = valuetokens
self.priority = prioritytokens
# also invalid values are set!
if self._isValidating():
self.validate()
else:
self._log.error(
'Property: No property name found: %s' % self._valuestr(cssText)
)
cssText = property(
fget=_getCssText, fset=_setCssText, doc="A parsable textual representation."
)
def _setName(self, name):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified name has a syntax error and is
unparsable.
"""
# for closures: must be a mutable
new = {'literalname': None, 'wellformed': True}
def _ident(expected, seq, token, tokenizer=None):
# name
if 'name' == expected:
new['literalname'] = self._tokenvalue(token).lower()
seq.append(new['literalname'])
return 'EOF'
else:
new['wellformed'] = False
self._log.error('Property: Unexpected ident.', token)
return expected
newseq = []
wellformed, expected = self._parse(
expected='name',
seq=newseq,
tokenizer=self._tokenize2(name),
productions={'IDENT': _ident},
)
wellformed = wellformed and new['wellformed']
# post conditions
# define a token for error logging
if isinstance(name, list):
token = name[0]
self.__nametoken = token
else:
token = None
if not new['literalname']:
wellformed = False
self._log.error(
'Property: No name found: %s' % self._valuestr(name), token=token
)
if wellformed:
self.wellformed = True
self._literalname = new['literalname']
self._name = self._normalize(self._literalname)
self.seqs[0] = newseq
# validate
if self._isValidating() and self._name not in cssutils.profile.knownNames:
# self.valid = False
self._log.warn(
'Property: Unknown Property name.', token=token, neverraise=True
)
else:
pass
# self.valid = True
# if self.propertyValue:
# self.propertyValue._propertyName = self._name
# #self.valid = self.propertyValue.valid
else:
self.wellformed = False
name = property(lambda self: self._name, _setName, doc="Name of this property.")
literalname = property(
lambda self: self._literalname,
doc="Readonly literal (not normalized) name " "of this property",
)
def _setPropertyValue(self, cssText):
"""
See css.PropertyValue
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
(according to the attached property) or is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
TODO: Raised if the specified CSS string value represents a different
type of values than the values allowed by the CSS property.
"""
if self._mediaQuery and not cssText:
self.seqs[1] = PropertyValue(parent=self)
else:
self.seqs[1].cssText = cssText
self.wellformed = self.wellformed and self.seqs[1].wellformed
propertyValue = property(
lambda self: self.seqs[1],
_setPropertyValue,
doc="(cssutils) PropertyValue object of property",
)
def _getValue(self):
if self.propertyValue:
# value without comments
return self.propertyValue.value
else:
return ''
def _setValue(self, value):
self._setPropertyValue(value)
value = property(
_getValue, _setValue, doc="The textual value of this Properties propertyValue."
)
def _setPriority(self, priority):
self.priority = priority
@property
def priority(self):
"""Priority of this property."""
return self._priority
@priority.setter
def priority(self, priority): # noqa: C901
"""
priority
a string, currently either u'', u'!important' or u'important'
Format::
prio
: IMPORTANT_SYM S*
;
"!"{w}"important" {return IMPORTANT_SYM;}
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified priority has a syntax error and is
unparsable.
In this case a priority not equal to None, "" or "!{w}important".
As CSSOM defines CSSStyleDeclaration.getPropertyPriority resulting
in u'important' this value is also allowed to set a Properties
priority
"""
if self._mediaQuery:
self._priority = ''
self._literalpriority = ''
if priority:
self._log.error('Property: No priority in a MediaQuery - ' 'ignored.')
return
if isinstance(priority, str) and 'important' == self._normalize(priority):
priority = '!%s' % priority
# for closures: must be a mutable
new = {'literalpriority': '', 'wellformed': True}
def _char(expected, seq, token, tokenizer=None):
# "!"
val = self._tokenvalue(token)
if '!' == expected == val:
seq.append(val)
return 'important'
else:
new['wellformed'] = False
self._log.error('Property: Unexpected char.', token)
return expected
def _ident(expected, seq, token, tokenizer=None):
# "important"
val = self._tokenvalue(token)
if 'important' == expected:
new['literalpriority'] = val
seq.append(val)
return 'EOF'
else:
new['wellformed'] = False
self._log.error('Property: Unexpected ident.', token)
return expected
newseq = []
wellformed, expected = self._parse(
expected='!',
seq=newseq,
tokenizer=self._tokenize2(priority),
productions={'CHAR': _char, 'IDENT': _ident},
)
wellformed = wellformed and new['wellformed']
# post conditions
if priority and not new['literalpriority']:
wellformed = False
self._log.info('Property: Invalid priority: %s' % self._valuestr(priority))
if wellformed:
self.wellformed = self.wellformed and wellformed
self._literalpriority = new['literalpriority']
self._priority = self._normalize(self.literalpriority)
self.seqs[2] = newseq
# validate priority
if self._priority not in ('', 'important'):
self._log.error('Property: No CSS priority value: %s' % self._priority)
literalpriority = property(
lambda self: self._literalpriority,
doc="Readonly literal (not normalized) priority of this property",
)
@property
def parent(self):
"""The Parent Node (normally a CSSStyledeclaration) of this Property"""
return self._parent
@parent.setter
def parent(self, parent):
self._parent = parent
def validate(self): # noqa: C901
"""Validate value against `profiles` which are checked dynamically.
properties in e.g. @font-face rules are checked against
``cssutils.profile.CSS3_FONT_FACE`` only.
For each of the following cases a message is reported:
- INVALID (so the property is known but not valid)
``ERROR Property: Invalid value for "{PROFILE-1[/PROFILE-2...]"
property: ...``
- VALID but not in given profiles or defaultProfiles
``WARNING Property: Not valid for profile "{PROFILE-X}" but valid
"{PROFILE-Y}" property: ...``
- VALID in current profile
``DEBUG Found valid "{PROFILE-1[/PROFILE-2...]" property...``
- UNKNOWN property
``WARNING Unknown Property name...`` is issued
so for example::
cssutils.log.setLevel(logging.DEBUG)
parser = cssutils.CSSParser()
s = parser.parseString('''body {
unknown-property: x;
color: 4;
color: rgba(1,2,3,4);
color: red
}''')
# Log output:
WARNING Property: Unknown Property name. [2:9: unknown-property]
ERROR Property: Invalid value for \
"CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color]
DEBUG Property: Found valid \
"CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color]
DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color]
and when setting an explicit default profile::
cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2
s = parser.parseString('''body {
unknown-property: x;
color: 4;
color: rgba(1,2,3,4);
color: red
}''')
# Log output:
WARNING Property: Unknown Property name. [2:9: unknown-property]
ERROR Property: Invalid value for \
"CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color]
WARNING Property: Not valid for profile \
"CSS Level 2.1" but valid "CSS Color Module Level 3" \
value: rgba(1, 2, 3, 4) [4:9: color]
DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color]
"""
valid = False
profiles = None
try:
# if @font-face use that profile
rule = self.parent.parentRule
except AttributeError:
pass
else:
if rule is not None:
if rule.type == rule.FONT_FACE_RULE:
profiles = [cssutils.profile.CSS3_FONT_FACE]
# TODO: same for @page
if self.name and self.value:
# TODO
# cv = self.propertyValue
# if cv.cssValueType == cv.CSS_VARIABLE and not cv.value:
# # TODO: false alarms too!
# cssutils.log.warn(u'No value for variable "%s" found, keeping '
# u'variable.' % cv.name, neverraise=True)
if self.name in cssutils.profile.knownNames:
# add valid, matching, validprofiles...
valid, matching, validprofiles = cssutils.profile.validateWithProfile(
self.name, self.value, profiles
)
if not valid:
self._log.error(
'Property: Invalid value for '
'"%s" property: %s' % ('/'.join(validprofiles), self.value),
token=self.__nametoken,
neverraise=True,
)
# TODO: remove logic to profiles!
elif (
valid and not matching
): # (profiles and profiles not in validprofiles):
if not profiles:
notvalidprofiles = '/'.join(cssutils.profile.defaultProfiles)
else:
notvalidprofiles = profiles
self._log.warn(
'Property: Not valid for profile "%s" '
'but valid "%s" value: %s '
% (notvalidprofiles, '/'.join(validprofiles), self.value),
token=self.__nametoken,
neverraise=True,
)
valid = False
elif valid:
self._log.debug(
'Property: Found valid "%s" value: %s'
% ('/'.join(validprofiles), self.value),
token=self.__nametoken,
neverraise=True,
)
if self._priority not in ('', 'important'):
valid = False
return valid
valid = property(
validate,
doc="Check if value of this property is valid " "in the properties context.",
)
@Deprecated('Use ``property.propertyValue`` instead.')
def _getCSSValue(self):
return self.propertyValue
@Deprecated('Use ``property.propertyValue`` instead.')
def _setCSSValue(self, cssText):
self._setPropertyValue(cssText)
cssValue = property(
_getCSSValue,
_setCSSValue,
doc="(DEPRECATED) Use ``property.propertyValue`` instead.",
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/selector.py 0000644 0001751 0000177 00000072764 14627633762 017745 0 ustar 00runner docker """Selector is a single Selector of a CSSStyleRule SelectorList.
Partly implements http://www.w3.org/TR/css3-selectors/.
TODO
- .contains(selector)
- .isSubselector(selector)
"""
from __future__ import annotations
__all__ = ['Selector']
import contextlib
import dataclasses
import xml.dom
import cssutils
from cssutils.helper import Deprecated
from cssutils.util import _SimpleNamespaces
class Constants:
"expected constants"
# used for equality checks and setting of a space combinator
S = ' '
simple_selector_sequence = (
'type_selector universal HASH class ' 'attrib pseudo negation '
)
simple_selector_sequence2 = 'HASH class attrib pseudo negation '
element_name = 'element_name'
negation_arg = 'type_selector universal HASH class attrib pseudo'
negationend = ')'
attname = 'prefix attribute'
attname2 = 'attribute'
attcombinator = 'combinator ]' # optional
attvalue = 'value' # optional
attend = ']'
expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
expression = expressionstart + ' )'
combinator = ' combinator'
@dataclasses.dataclass
class New(cssutils.util._BaseClass):
"""
Derives from _BaseClass to provide self._log.
"""
selector: Selector
namespaces: dict[str, str]
context: list[str] = dataclasses.field(default_factory=lambda: [''])
"stack of: 'attrib', 'negation', 'pseudo'"
element: str | None = None
_PREFIX: str | None = None
specificity: list[int] = dataclasses.field(default_factory=lambda: [0] * 4)
"mutable, finally a tuple!"
wellformed: bool = True
def append(self, seq, val, typ=None, token=None): # noqa: C901
"""
appends to seq
namespace_prefix, IDENT will be combined to a tuple
(prefix, name) where prefix might be None, the empty string
or a prefix.
Saved are also:
- specificity definition: style, id, class/att, type
- element: the element this Selector is for
"""
context = self.context[-1]
if token:
line, col = token[2], token[3]
else:
line, col = None, None
if typ == '_PREFIX':
# SPECIAL TYPE: save prefix for combination with next
self._PREFIX = val[:-1]
# handle next time
return
if self._PREFIX is not None:
# as saved from before and reset to None
prefix, self._PREFIX = self._PREFIX, None
elif typ == 'universal' and '|' in val:
# val == *|* or prefix|*
prefix, val = val.split('|')
else:
prefix = None
# namespace
if (typ.endswith('-selector') or typ == 'universal') and not (
'attribute-selector' == typ and not prefix
):
# att **IS NOT** in default ns
if prefix == '*':
# *|name: in ANY_NS
namespaceURI = cssutils._ANYNS
elif prefix is None:
# e or *: default namespace with prefix u''
# or local-name()
namespaceURI = self.namespaces.get('', None)
elif prefix == '':
# |name or |*: in no (or the empty) namespace
namespaceURI = ''
else:
# explicit namespace prefix
# does not raise KeyError, see _SimpleNamespaces
namespaceURI = self.namespaces[prefix]
if namespaceURI is None:
self.wellformed = False
self._log.error(
'Selector: No namespaceURI found ' 'for prefix %r' % prefix,
token=token,
error=xml.dom.NamespaceErr,
)
return
# val is now (namespaceprefix, name) tuple
val = (namespaceURI, val)
# specificity
if not context or context == 'negation':
if 'id' == typ:
self.specificity[1] += 1
elif 'class' == typ or '[' == val:
self.specificity[2] += 1
elif typ in (
'type-selector',
'negation-type-selector',
'pseudo-element',
):
self.specificity[3] += 1
if not context and typ in ('type-selector', 'universal'):
# define element
self.element = val
seq.append(val, typ, line=line, col=col)
def _COMMENT(self, expected, seq, token, tokenizer=None):
"special implementation for comment token"
self.append(seq, cssutils.css.CSSComment([token]), 'COMMENT', token=token)
return expected
def _S(self, expected, seq, token, tokenizer=None):
# S
context = self.context[-1]
if context.startswith('pseudo-'):
if seq and seq[-1].value not in '+-':
# e.g. x:func(a + b)
self.append(seq, Constants.S, 'S', token=token)
return expected
elif context != 'attrib' and 'combinator' in expected:
self.append(seq, Constants.S, 'descendant', token=token)
return Constants.simple_selector_sequence + Constants.combinator
else:
return expected
def _universal(self, expected, seq, token, tokenizer=None):
# *|* or prefix|*
context = self.context[-1]
val = self.selector._tokenvalue(token)
if 'universal' in expected:
self.append(seq, val, 'universal', token=token)
if 'negation' == context:
return Constants.negationend
else:
return Constants.simple_selector_sequence2 + Constants.combinator
else:
self.wellformed = False
self._log.error('Selector: Unexpected universal.', token=token)
return expected
def _namespace_prefix(self, expected, seq, token, tokenizer=None):
# prefix| => element_name
# or prefix| => attribute_name if attrib
context = self.context[-1]
val = self.selector._tokenvalue(token)
if 'attrib' == context and 'prefix' in expected:
# [PREFIX|att]
self.append(seq, val, '_PREFIX', token=token)
return Constants.attname2
elif 'type_selector' in expected:
# PREFIX|*
self.append(seq, val, '_PREFIX', token=token)
return Constants.element_name
else:
self.wellformed = False
self._log.error('Selector: Unexpected namespace prefix.', token=token)
return expected
def _pseudo(self, expected, seq, token, tokenizer=None):
# pseudo-class or pseudo-element :a ::a :a( ::a(
"""
/* '::' starts a pseudo-element, ':' a pseudo-class */
/* Exceptions: :first-line, :first-letter, :before and
:after. */
/* Note that pseudo-elements are restricted to one per selector
and */
/* occur only in the last simple_selector_sequence. */
"""
context = self.context[-1]
val, typ = (
self.selector._tokenvalue(token, normalize=True),
self.selector._type(token),
)
if 'pseudo' in expected:
if val in (':first-line', ':first-letter', ':before', ':after'):
# always pseudo-element ???
typ = 'pseudo-element'
self.append(seq, val, typ, token=token)
if val.endswith('('):
# function
# "pseudo-" "class" or "element"
self.context.append(typ)
return Constants.expressionstart
elif 'negation' == context:
return Constants.negationend
elif 'pseudo-element' == typ:
# only one per element, check at ) also!
return Constants.combinator
else:
return Constants.simple_selector_sequence2 + Constants.combinator
else:
self.wellformed = False
self._log.error('Selector: Unexpected start of pseudo.', token=token)
return expected
def _expression(self, expected, seq, token, tokenizer=None):
# [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
context = self.context[-1]
val, typ = self.selector._tokenvalue(token), self.selector._type(token)
if context.startswith('pseudo-'):
self.append(seq, val, typ, token=token)
return Constants.expression
else:
self.wellformed = False
self._log.error('Selector: Unexpected %s.' % typ, token=token)
return expected
def _attcombinator(self, expected, seq, token, tokenizer=None):
# context: attrib
# PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES |
# DASHMATCH
context = self.context[-1]
val, typ = self.selector._tokenvalue(token), self.selector._type(token)
if 'attrib' == context and 'combinator' in expected:
# combinator in attrib
self.append(seq, val, typ.lower(), token=token)
return Constants.attvalue
else:
self.wellformed = False
self._log.error('Selector: Unexpected %s.' % typ, token=token)
return expected
def _string(self, expected, seq, token, tokenizer=None):
# identifier
context = self.context[-1]
typ, val = self.selector._type(token), self.selector._stringtokenvalue(token)
# context: attrib
if 'attrib' == context and 'value' in expected:
# attrib: [...=VALUE]
self.append(seq, val, typ, token=token)
return Constants.attend
# context: pseudo
elif context.startswith('pseudo-'):
# :func(...)
self.append(seq, val, typ, token=token)
return Constants.expression
else:
self.wellformed = False
self._log.error('Selector: Unexpected STRING.', token=token)
return expected
def _ident(self, expected, seq, token, tokenizer=None):
# identifier
context = self.context[-1]
val, typ = self.selector._tokenvalue(token), self.selector._type(token)
# context: attrib
if 'attrib' == context and 'attribute' in expected:
# attrib: [...|ATT...]
self.append(seq, val, 'attribute-selector', token=token)
return Constants.attcombinator
elif 'attrib' == context and 'value' in expected:
# attrib: [...=VALUE]
self.append(seq, val, 'attribute-value', token=token)
return Constants.attend
# context: negation
elif 'negation' == context:
# negation: (prefix|IDENT)
self.append(seq, val, 'negation-type-selector', token=token)
return Constants.negationend
# context: pseudo
elif context.startswith('pseudo-'):
# :func(...)
self.append(seq, val, typ, token=token)
return Constants.expression
elif 'type_selector' in expected or Constants.element_name == expected:
# element name after ns or complete type_selector
self.append(seq, val, 'type-selector', token=token)
return Constants.simple_selector_sequence2 + Constants.combinator
else:
self.wellformed = False
self._log.error('Selector: Unexpected IDENT.', token=token)
return expected
def _class(self, expected, seq, token, tokenizer=None):
# .IDENT
context = self.context[-1]
val = self.selector._tokenvalue(token)
if 'class' in expected:
self.append(seq, val, 'class', token=token)
if 'negation' == context:
return Constants.negationend
else:
return Constants.simple_selector_sequence2 + Constants.combinator
else:
self.wellformed = False
self._log.error('Selector: Unexpected class.', token=token)
return expected
def _hash(self, expected, seq, token, tokenizer=None):
# #IDENT
context = self.context[-1]
val = self.selector._tokenvalue(token)
if 'HASH' in expected:
self.append(seq, val, 'id', token=token)
if 'negation' == context:
return Constants.negationend
else:
return Constants.simple_selector_sequence2 + Constants.combinator
else:
self.wellformed = False
self._log.error('Selector: Unexpected HASH.', token=token)
return expected
def _char(self, expected, seq, token, tokenizer=None): # noqa: C901
# + > ~ ) [ ] + -
context = self.context[-1]
val = self.selector._tokenvalue(token)
# context: attrib
if ']' == val and 'attrib' == context and ']' in expected:
# end of attrib
self.append(seq, val, 'attribute-end', token=token)
context = self.context.pop() # attrib is done
context = self.context[-1]
if 'negation' == context:
return Constants.negationend
else:
return Constants.simple_selector_sequence2 + Constants.combinator
if '=' == val and 'attrib' == context and 'combinator' in expected:
# combinator in attrib
self.append(seq, val, 'equals', token=token)
return Constants.attvalue
# context: negation
if ')' == val and 'negation' == context and ')' in expected:
# not(negation_arg)"
self.append(seq, val, 'negation-end', token=token)
self.context.pop() # negation is done
context = self.context[-1]
return Constants.simple_selector_sequence + Constants.combinator
# context: pseudo (at least one expression)
if val in '+-' and context.startswith('pseudo-'):
# :func(+ -)"
_names = {'+': 'plus', '-': 'minus'}
if val == '+' and seq and seq[-1].value == Constants.S:
seq.replace(-1, val, _names[val])
else:
self.append(seq, val, _names[val], token=token)
return Constants.expression
if (
')' == val
and context.startswith('pseudo-')
and Constants.expression == expected
):
# :func(expression)"
self.append(seq, val, 'function-end', token=token)
self.context.pop() # pseudo is done
if 'pseudo-element' == context:
return Constants.combinator
else:
return Constants.simple_selector_sequence + Constants.combinator
# context: ROOT
if '[' == val and 'attrib' in expected:
# start of [attrib]
self.append(seq, val, 'attribute-start', token=token)
self.context.append('attrib')
return Constants.attname
if val in '+>~' and 'combinator' in expected:
# no other combinator except S may be following
_names = {
'>': 'child',
'+': 'adjacent-sibling',
'~': 'following-sibling',
}
if seq and seq[-1].value == Constants.S:
seq.replace(-1, val, _names[val])
else:
self.append(seq, val, _names[val], token=token)
return Constants.simple_selector_sequence
if ',' == val:
# not a selectorlist
self.wellformed = False
self._log.error(
'Selector: Single selector only.',
error=xml.dom.InvalidModificationErr,
token=token,
)
return expected
self.wellformed = False
self._log.error('Selector: Unexpected CHAR.', token=token)
return expected
def _negation(self, expected, seq, token, tokenizer=None):
# not(
val = self.selector._tokenvalue(token, normalize=True)
if 'negation' in expected:
self.context.append('negation')
self.append(seq, val, 'negation-start', token=token)
return Constants.negation_arg
else:
self.wellformed = False
self._log.error('Selector: Unexpected negation.', token=token)
return expected
def _atkeyword(self, expected, seq, token, tokenizer=None):
"invalidates selector"
self.wellformed = False
self._log.error('Selector: Unexpected ATKEYWORD.', token=token)
return expected
@property
def productions(self):
return {
'CHAR': self._char,
'class': self._class,
'HASH': self._hash,
'STRING': self._string,
'IDENT': self._ident,
'namespace_prefix': self._namespace_prefix,
'negation': self._negation,
'pseudo-class': self._pseudo,
'pseudo-element': self._pseudo,
'universal': self._universal,
# pseudo
'NUMBER': self._expression,
'DIMENSION': self._expression,
# attribute
'PREFIXMATCH': self._attcombinator,
'SUFFIXMATCH': self._attcombinator,
'SUBSTRINGMATCH': self._attcombinator,
'DASHMATCH': self._attcombinator,
'INCLUDES': self._attcombinator,
'S': self._S,
'COMMENT': self._COMMENT,
'ATKEYWORD': self._atkeyword,
}
class Selector(cssutils.util.Base2):
"""
(cssutils) a single selector in a :class:`~cssutils.css.SelectorList`
of a :class:`~cssutils.css.CSSStyleRule`.
Format::
# implemented in SelectorList
selectors_group
: selector [ COMMA S* selector ]*
;
selector
: simple_selector_sequence [ combinator simple_selector_sequence ]*
;
combinator
/* combinators can be surrounded by white space */
: PLUS S* | GREATER S* | TILDE S* | S+
;
simple_selector_sequence
: [ type_selector | universal ]
[ HASH | class | attrib | pseudo | negation ]*
| [ HASH | class | attrib | pseudo | negation ]+
;
type_selector
: [ namespace_prefix ]? element_name
;
namespace_prefix
: [ IDENT | '*' ]? '|'
;
element_name
: IDENT
;
universal
: [ namespace_prefix ]? '*'
;
class
: '.' IDENT
;
attrib
: '[' S* [ namespace_prefix ]? IDENT S*
[ [ PREFIXMATCH |
SUFFIXMATCH |
SUBSTRINGMATCH |
'=' |
INCLUDES |
DASHMATCH ] S* [ IDENT | STRING ] S*
]? ']'
;
pseudo
/* '::' starts a pseudo-element, ':' a pseudo-class */
/* Exceptions: :first-line, :first-letter, :before and :after. */
/* Note that pseudo-elements are restricted to one per selector and */
/* occur only in the last simple_selector_sequence. */
: ':' ':'? [ IDENT | functional_pseudo ]
;
functional_pseudo
: FUNCTION S* expression ')'
;
expression
/* In CSS3, the expressions are identifiers, strings, */
/* or of the form "an+b" */
: [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
;
negation
: NOT S* negation_arg S* ')'
;
negation_arg
: type_selector | universal | HASH | class | attrib | pseudo
;
"""
def __init__(self, selectorText=None, parent=None, readonly=False):
"""
:Parameters:
selectorText
initial value of this selector
parent
a SelectorList
readonly
default to False
"""
super().__init__()
self.__namespaces = _SimpleNamespaces(log=self._log)
self._element = None
self._parent = parent
self._specificity = (0, 0, 0, 0)
if selectorText:
self.selectorText = selectorText
self._readonly = readonly
def __repr__(self):
if self.__getNamespaces():
st = (self.selectorText, self._getUsedNamespaces())
else:
st = self.selectorText
return f"cssutils.css.{self.__class__.__name__}(selectorText={st!r})"
def __str__(self):
return (
""
% (
self.__class__.__name__,
self.selectorText,
self.specificity,
self._getUsedNamespaces(),
id(self),
)
)
def _getUsedUris(self):
"Return list of actually used URIs in this Selector."
uris = set()
for item in self.seq:
type_, val = item.type, item.value
if (
type_.endswith('-selector')
or type_ == 'universal'
and isinstance(val, tuple)
and val[0] not in (None, '*')
):
uris.add(val[0])
return uris
def _getUsedNamespaces(self):
"Return actually used namespaces only."
useduris = self._getUsedUris()
namespaces = _SimpleNamespaces(log=self._log)
for p, uri in list(self._namespaces.items()):
if uri in useduris:
namespaces[p] = uri
return namespaces
def __getNamespaces(self):
"Use own namespaces if not attached to a sheet, else the sheet's ones."
try:
return self._parent.parentRule.parentStyleSheet.namespaces
except AttributeError:
return self.__namespaces
_namespaces = property(
__getNamespaces,
doc="If this Selector is attached to a "
"CSSStyleSheet the namespaces of that sheet "
"are mirrored here. While the Selector (or "
"parent SelectorList or parentRule(s) of that "
"are not attached a own dict of {prefix: "
"namespaceURI} is used.",
)
@property
def element(self):
"""Effective element target of this selector."""
return self._element
parent = property(
lambda self: self._parent,
doc="(DOM) The SelectorList that contains this Selector "
"or None if this Selector is not attached to a "
"SelectorList.",
)
def _getSelectorText(self):
"""Return serialized format."""
return cssutils.ser.do_css_Selector(self)
def _setSelectorText(self, selectorText):
"""
:param selectorText:
parsable string or a tuple of (selectorText, dict-of-namespaces).
Given namespaces are ignored if this object is attached to a
CSSStyleSheet!
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if the specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
and is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
# might be (selectorText, namespaces)
selectorText, namespaces = self._splitNamespacesOff(selectorText)
with contextlib.suppress(AttributeError):
# uses parent stylesheets namespaces if available,
# otherwise given ones
namespaces = self.parent.parentRule.parentStyleSheet.namespaces
tokenizer = self._tokenize2(selectorText)
if not tokenizer:
self._log.error('Selector: No selectorText given.')
return
tokenizer = self._prepare_tokens(tokenizer)
new = New(selector=self, namespaces=namespaces)
# expected: only|not or mediatype, mediatype, feature, and
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected=Constants.simple_selector_sequence,
seq=newseq,
tokenizer=tokenizer,
productions=new.productions,
)
wellformed = wellformed and new.wellformed
# post condition
if len(new.context) > 1 or not newseq:
wellformed = False
self._log.error(
'Selector: Invalid or incomplete selector: %s'
% self._valuestr(selectorText)
)
if expected == 'element_name':
wellformed = False
self._log.error(
'Selector: No element name found: %s' % self._valuestr(selectorText)
)
if expected == Constants.simple_selector_sequence and newseq:
wellformed = False
self._log.error(
'Selector: Cannot end with combinator: %s'
% self._valuestr(selectorText)
)
if (
newseq
and hasattr(newseq[-1].value, 'strip')
and newseq[-1].value.strip() == ''
):
del newseq[-1]
# set
if wellformed:
self.__namespaces = namespaces
self._element = new.element
self._specificity = tuple(new.specificity)
self._setSeq(newseq)
# filter that only used ones are kept
self.__namespaces = self._getUsedNamespaces()
def _prepare_tokens(self, tokenizer): # noqa: C901
"""
"*" -> type "universal"
"*"|IDENT + "|" -> combined to "namespace_prefix"
"|" -> type "namespace_prefix"
"." + IDENT -> combined to "class"
":" + IDENT, ":" + FUNCTION -> pseudo-class
FUNCTION "not(" -> negation
"::" + IDENT, "::" + FUNCTION -> pseudo-element
"""
tokens = []
for t in tokenizer:
typ, val, lin, col = t
if val == ':' and tokens and self._tokenvalue(tokens[-1]) == ':':
# combine ":" and ":"
tokens[-1] = (typ, '::', lin, col)
elif typ == 'IDENT' and tokens and self._tokenvalue(tokens[-1]) == '.':
# class: combine to .IDENT
tokens[-1] = ('class', '.' + val, lin, col)
elif (
typ == 'IDENT'
and tokens
and self._tokenvalue(tokens[-1]).startswith(':')
and not self._tokenvalue(tokens[-1]).endswith('(')
):
# pseudo-X: combine to :IDENT or ::IDENT but not ":a(" + "b"
if self._tokenvalue(tokens[-1]).startswith('::'):
t = 'pseudo-element'
else:
t = 'pseudo-class'
tokens[-1] = (t, self._tokenvalue(tokens[-1]) + val, lin, col)
elif (
typ == 'FUNCTION'
and val == 'not('
and tokens
and ':' == self._tokenvalue(tokens[-1])
):
tokens[-1] = ('negation', ':' + val, lin, tokens[-1][3])
elif (
typ == 'FUNCTION'
and tokens
and self._tokenvalue(tokens[-1]).startswith(':')
):
# pseudo-X: combine to :FUNCTION( or ::FUNCTION(
if self._tokenvalue(tokens[-1]).startswith('::'):
t = 'pseudo-element'
else:
t = 'pseudo-class'
tokens[-1] = (t, self._tokenvalue(tokens[-1]) + val, lin, col)
elif (
val == '*'
and tokens
and self._type(tokens[-1]) == 'namespace_prefix'
and self._tokenvalue(tokens[-1]).endswith('|')
):
# combine prefix|*
tokens[-1] = (
'universal',
self._tokenvalue(tokens[-1]) + val,
lin,
col,
)
elif val == '*':
# universal: "*"
tokens.append(('universal', val, lin, col))
elif (
val == '|'
and tokens
and self._type(tokens[-1]) in (self._prods.IDENT, 'universal')
and self._tokenvalue(tokens[-1]).find('|') == -1
):
# namespace_prefix: "IDENT|" or "*|"
tokens[-1] = (
'namespace_prefix',
self._tokenvalue(tokens[-1]) + '|',
lin,
col,
)
elif val == '|':
# namespace_prefix: "|"
tokens.append(('namespace_prefix', val, lin, col))
else:
tokens.append(t)
return iter(tokens)
selectorText = property(
_getSelectorText,
_setSelectorText,
doc="(DOM) The parsable textual representation of " "the selector.",
)
specificity = property(
lambda self: self._specificity,
doc="""Specificity of this selector (READONLY).
Tuple of (a, b, c, d) where:
a
presence of style in document, always 0 if not used on a
document
b
number of ID selectors
c
number of .class selectors
d
number of Element (type) selectors""",
)
wellformed = property(lambda self: bool(len(self.seq)))
@Deprecated('Use property parent instead')
def _getParentList(self):
return self.parent
parentList = property(_getParentList, doc="DEPRECATED, see property parent instead")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/selectorlist.py 0000644 0001751 0000177 00000020260 14627633762 020621 0 ustar 00runner docker """SelectorList is a list of CSS Selector objects.
TODO
- remove duplicate Selectors. -> CSSOM canonicalize
- ??? CSS2 gives a special meaning to the comma (,) in selectors.
However, since it is not known if the comma may acquire other
meanings in future versions of CSS, the whole statement should be
ignored if there is an error anywhere in the selector, even though
the rest of the selector may look reasonable in CSS2.
Illegal example(s):
For example, since the "&" is not a valid token in a CSS2 selector,
a CSS2 user agent must ignore the whole second line, and not set
the color of H3 to red:
"""
__all__ = ['SelectorList']
import cssutils
from .selector import Selector
class SelectorList(cssutils.util.Base, cssutils.util.ListSeq):
"""A list of :class:`~cssutils.css.Selector` objects
of a :class:`~cssutils.css.CSSStyleRule`."""
def __init__(self, selectorText=None, parentRule=None, readonly=False):
"""
:Parameters:
selectorText
parsable list of Selectors
parentRule
the parent CSSRule if available
"""
super().__init__()
self._parentRule = parentRule
if selectorText:
self.selectorText = selectorText
self._readonly = readonly
def __repr__(self):
if self._namespaces:
st = (self.selectorText, self._namespaces)
else:
st = self.selectorText
return f"cssutils.css.{self.__class__.__name__}(selectorText={st!r})"
def __str__(self):
return "" % (
self.__class__.__name__,
self.selectorText,
self._namespaces,
id(self),
)
def __setitem__(self, index, newSelector):
"""Overwrite ListSeq.__setitem__
Any duplicate Selectors are **not** removed.
"""
newSelector = self.__prepareset(newSelector)
if newSelector:
self.seq[index] = newSelector
def __prepareset(self, newSelector, namespaces=None):
"Used by appendSelector and __setitem__"
if not namespaces:
namespaces = {}
self._checkReadonly()
if not isinstance(newSelector, Selector):
newSelector = Selector((newSelector, namespaces), parent=self)
if newSelector.wellformed:
newSelector._parent = self # maybe set twice but must be!
return newSelector
def __getNamespaces(self):
"""Use children namespaces if not attached to a sheet, else the sheet's
ones.
"""
try:
return self.parentRule.parentStyleSheet.namespaces
except AttributeError:
namespaces = {}
for selector in self.seq:
namespaces.update(selector._namespaces)
return namespaces
def _getUsedUris(self):
"Used by CSSStyleSheet to check if @namespace rules are needed"
uris = set()
for s in self:
uris.update(s._getUsedUris())
return uris
_namespaces = property(
__getNamespaces,
doc="""If this SelectorList is
attached to a CSSStyleSheet the namespaces of that sheet are mirrored
here. While the SelectorList (or parentRule(s) are
not attached the namespaces of all children Selectors are used.""",
)
def append(self, newSelector):
"Same as :meth:`appendSelector`."
self.appendSelector(newSelector)
def appendSelector(self, newSelector):
"""
Append `newSelector` to this list (a string will be converted to a
:class:`~cssutils.css.Selector`).
:param newSelector:
comma-separated list of selectors (as a single string) or a tuple of
`(newSelector, dict-of-namespaces)`
:returns: New :class:`~cssutils.css.Selector` or ``None`` if
`newSelector` is not wellformed.
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if the specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
and is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
# might be (selectorText, namespaces)
newSelector, namespaces = self._splitNamespacesOff(newSelector)
try:
# use parent's only if available
namespaces = self.parentRule.parentStyleSheet.namespaces
except AttributeError:
# use already present namespaces plus new given ones
_namespaces = self._namespaces
_namespaces.update(namespaces)
namespaces = _namespaces
newSelector = self.__prepareset(newSelector, namespaces)
if newSelector:
seq = self.seq[:]
del self.seq[:]
for s in seq:
if s.selectorText != newSelector.selectorText:
self.seq.append(s)
self.seq.append(newSelector)
return newSelector
def _getSelectorText(self):
"Return serialized format."
return cssutils.ser.do_css_SelectorList(self)
def _setSelectorText(self, selectorText):
"""
:param selectorText:
comma-separated list of selectors or a tuple of
(selectorText, dict-of-namespaces)
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if the specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
and is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
# might be (selectorText, namespaces)
selectorText, namespaces = self._splitNamespacesOff(selectorText)
try:
# use parent's only if available
namespaces = self.parentRule.parentStyleSheet.namespaces
except AttributeError:
pass
wellformed = True
tokenizer = self._tokenize2(selectorText)
newseq = []
expected = True
while True:
# find all upto and including next ",", EOF or nothing
selectortokens = self._tokensupto2(tokenizer, listseponly=True)
if selectortokens:
if self._tokenvalue(selectortokens[-1]) == ',':
expected = selectortokens.pop()
else:
expected = None
selector = Selector((selectortokens, namespaces), parent=self)
if selector.wellformed:
newseq.append(selector)
else:
wellformed = False
self._log.error(
'SelectorList: Invalid Selector: %s'
% self._valuestr(selectortokens)
)
else:
break
# post condition
if ',' == expected:
wellformed = False
self._log.error(
'SelectorList: Cannot end with ",": %r' % self._valuestr(selectorText)
)
elif expected:
wellformed = False
self._log.error(
'SelectorList: Unknown Syntax: %r' % self._valuestr(selectorText)
)
if wellformed:
self.seq = newseq
selectorText = property(
_getSelectorText,
_setSelectorText,
doc="(cssutils) The textual representation of the " "selector for a rule set.",
)
length = property(
lambda self: len(self),
doc="The number of :class:`~cssutils.css.Selector` " "objects in the list.",
)
parentRule = property(
lambda self: self._parentRule,
doc="(DOM) The CSS rule that contains this "
"SelectorList or ``None`` if this SelectorList "
"is not attached to a CSSRule.",
)
wellformed = property(lambda self: bool(len(self.seq)))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css/value.py 0000644 0001751 0000177 00000077023 14627633762 017232 0 ustar 00runner docker """Value related classes.
DOM Level 2 CSS CSSValue, CSSPrimitiveValue and CSSValueList are **no longer**
supported and are replaced by these new classes.
"""
__all__ = [
'PropertyValue',
'Value',
'ColorValue',
'DimensionValue',
'URIValue',
'CSSFunction',
'CSSCalc',
'CSSVariable',
'MSValue',
]
import colorsys
import re
import urllib.parse
import cssutils
from cssutils.helper import normalize, pushtoken
from cssutils.prodparser import Choice, PreDef, Prod, ProdParser, Sequence
class PropertyValue(cssutils.util._NewBase):
"""
An unstructured list like holder for all values defined for a
:class:`~cssutils.css.Property`. Contains :class:`~cssutils.css.Value`
or subclass objects. Currently there is no access to the combinators of
the defined values which might simply be space or comma or slash.
You may:
- iterate over all contained Value objects (not the separators like ``,``,
``/`` or `` `` though!)
- get a Value item by index or use ``PropertyValue[index]``
- find out the number of values defined (unstructured)
"""
def __init__(self, cssText=None, parent=None, readonly=False):
"""
:param cssText:
the parsable cssText of the value
:param readonly:
defaults to False
"""
super().__init__()
self.parent = parent
self.wellformed = False
if cssText is not None: # may be 0
if isinstance(cssText, (int, float)):
cssText = str(cssText) # if it is a number
self.cssText = cssText
self._readonly = readonly
def __len__(self):
return len(list(self.__items()))
def __getitem__(self, index):
try:
return list(self.__items())[index]
except IndexError:
return None
def __iter__(self):
"Generator which iterates over values."
yield from self.__items()
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}({self.cssText!r})"
def __str__(self):
return "" % (
self.__class__.__name__,
self.length,
self.cssText,
id(self),
)
def __items(self, seq=None):
"a generator of Value obects only, no , / or ' '"
if seq is None:
seq = self.seq
return (x.value for x in seq if isinstance(x.value, Value))
def _setCssText(self, cssText):
if isinstance(cssText, (int, float)):
cssText = str(cssText) # if it is a number
"""
Format::
unary_operator
: '-' | '+'
;
operator
: '/' S* | ',' S* | /* empty */
;
expr
: term [ operator term ]*
;
term
: unary_operator?
[ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
ANGLE S* | TIME S* | FREQ S* ]
| STRING S* | IDENT S* | URI S* | hexcolor | function
| UNICODE-RANGE S*
;
function
: FUNCTION S* expr ')' S*
;
/*
* There is a constraint on the color that it must
* have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
* after the "#"; e.g., "#000" is OK, but "#abcd" is not.
*/
hexcolor
: HASH S*
;
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
(according to the attached property) or is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
TODO: Raised if the specified CSS string value represents a
different type of values than the values allowed by the CSS
property.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this value is readonly.
"""
self._checkReadonly()
# used as operator is , / or S
nextSor = ',/'
term = Choice(
_ColorProd(self, nextSor),
_DimensionProd(self, nextSor),
_URIProd(self, nextSor),
_ValueProd(self, nextSor),
# _Rect(self, nextSor),
# all other functions
_CSSVariableProd(self, nextSor),
_MSValueProd(self, nextSor),
_CalcValueProd(self, nextSor),
_CSSFunctionProd(self, nextSor),
)
operator = Choice(
PreDef.S(toSeq=False),
PreDef.char(
'comma', ',', toSeq=lambda t, tokens: ('operator', t[1]), optional=True
),
PreDef.char(
'slash', '/', toSeq=lambda t, tokens: ('operator', t[1]), optional=True
),
optional=True,
)
prods = Sequence(
term,
Sequence( # mayEnd this Sequence if whitespace
operator,
# TODO: only when setting via other class
# used by variabledeclaration currently
PreDef.char('END', ';', stopAndKeep=True, optional=True),
# TODO: } and !important ends too!
term,
minmax=lambda: (0, None),
),
)
# parse
ok, seq, store, unused = ProdParser().parse(cssText, 'PropertyValue', prods)
# must be at least one value!
ok = ok and len(list(self.__items(seq))) > 0
for item in seq:
if hasattr(item.value, 'wellformed') and not item.value.wellformed:
ok = False
break
self.wellformed = ok
if ok:
self._setSeq(seq)
else:
self._log.error(
'PropertyValue: Unknown syntax or no value: %s'
% self._valuestr(cssText)
)
cssText = property(
lambda self: cssutils.ser.do_css_PropertyValue(self),
_setCssText,
doc="A string representation of the current value.",
)
def item(self, index):
"""
The value at position `index`. Alternatively simple use
``PropertyValue[index]``.
:param index:
the parsable cssText of the value
:exceptions:
- :exc:`~IndexError`:
Raised if index if out of bounds
"""
return self[index]
length = property(lambda self: len(self), doc="Number of values set.")
value = property(
lambda self: cssutils.ser.do_css_PropertyValue(self, valuesOnly=True),
doc="A string representation of the current value "
"without any comments used for validation.",
)
class Value(cssutils.util._NewBase):
"""
Represents a single CSS value. For now simple values of
IDENT, STRING, or UNICODE-RANGE values are represented directly
as Value objects. Other values like e.g. FUNCTIONs are represented by
subclasses with an extended API.
"""
IDENT = 'IDENT'
STRING = 'STRING'
UNICODE_RANGE = 'UNICODE-RANGE'
URI = 'URI'
DIMENSION = 'DIMENSION'
NUMBER = 'NUMBER'
PERCENTAGE = 'PERCENTAGE'
COLOR_VALUE = 'COLOR_VALUE'
HASH = 'HASH'
FUNCTION = 'FUNCTION'
CALC = 'CALC'
VARIABLE = 'VARIABLE'
_type = None
_value = ''
def __init__(self, cssText=None, parent=None, readonly=False):
super().__init__()
self.parent = parent
self.wellformed = False
if cssText:
self.cssText = cssText
def __repr__(self):
return f"cssutils.css.{self.__class__.__name__}({self.cssText!r})"
def __str__(self):
return f""
def _setCssText(self, cssText):
self._checkReadonly()
prods = Choice(
PreDef.hexcolor(stop=True),
PreDef.ident(stop=True),
PreDef.string(stop=True),
PreDef.unicode_range(stop=True),
)
ok, seq, store, unused = ProdParser().parse(cssText, 'Value', prods)
self.wellformed = ok
if ok:
# only 1 value anyway!
self._type = seq[0].type
self._value = seq[0].value
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_Value(self),
_setCssText,
doc='String value of this value.',
)
@property
def type(self):
"""
Type of this value, for now the production type
like e.g. `DIMENSION` or `STRING`. All types are
defined as constants in :class:`~cssutils.css.Value`.
"""
return self._type
def _setValue(self, value):
# TODO: check!
self._value = value
value = property(
lambda self: self._value,
_setValue,
doc="Actual value if possible: An int or float or else " " a string",
)
class ColorValue(Value):
"""
A color value like rgb(), rgba(), hsl(), hsla() or #rgb, #rrggbb
TODO: Color Keywords
"""
from .colors import COLORS
type = Value.COLOR_VALUE
# hexcolor, FUNCTION?
_colorType = None
_red = 0
_green = 0
_blue = 0
_alpha = 0
def __str__(self):
return (
""
% (
self.__class__.__name__,
self.type,
self.value,
self.colorType,
self.red,
self.green,
self.blue,
self.alpha,
id(self),
)
)
def _setCssText(self, cssText): # noqa: C901
self._checkReadonly()
types = self._prods # rename!
component = Choice(
PreDef.unary(
toSeq=lambda t, tokens: (
t[0],
DimensionValue(pushtoken(t, tokens), parent=self),
)
),
PreDef.number(
toSeq=lambda t, tokens: (
t[0],
DimensionValue(pushtoken(t, tokens), parent=self),
)
),
PreDef.percentage(
toSeq=lambda t, tokens: (
t[0],
DimensionValue(pushtoken(t, tokens), parent=self),
)
),
)
noalp = Sequence(
Prod(
name='FUNCTION',
match=lambda t, v: t == types.FUNCTION and v in ('rgb(', 'hsl('),
toSeq=lambda t, tokens: (t[0], normalize(t[1])),
),
component,
Sequence(PreDef.comma(optional=True), component, minmax=lambda: (2, 2)),
PreDef.funcEnd(stop=True),
)
witha = Sequence(
Prod(
name='FUNCTION',
match=lambda t, v: t == types.FUNCTION and v in ('rgba(', 'hsla('),
toSeq=lambda t, tokens: (t[0], normalize(t[1])),
),
component,
Sequence(PreDef.comma(optional=True), component, minmax=lambda: (3, 3)),
PreDef.funcEnd(stop=True),
)
namedcolor = Prod(
name='Named Color',
match=lambda t, v: t == 'IDENT'
and (normalize(v) in list(self.COLORS.keys())),
stop=True,
)
prods = Choice(PreDef.hexcolor(stop=True), namedcolor, noalp, witha)
ok, seq, store, unused = ProdParser().parse(cssText, self.type, prods)
self.wellformed = ok
if ok:
t, v = seq[0].type, seq[0].value
if 'IDENT' == t:
rgba = self.COLORS[normalize(v)]
if 'HASH' == t:
if len(v) == 4:
# HASH #rgb
rgba = (
int(2 * v[1], 16),
int(2 * v[2], 16),
int(2 * v[3], 16),
1.0,
)
else:
# HASH #rrggbb
rgba = (int(v[1:3], 16), int(v[3:5], 16), int(v[5:7], 16), 1.0)
elif 'FUNCTION' == t:
functiontype, raw, check = None, [], ''
HSL = False
for item in seq:
try:
type_ = item.value.type
except AttributeError:
# type of function, e.g. rgb(
if item.type == 'FUNCTION':
functiontype = item.value
HSL = functiontype in ('hsl(', 'hsla(')
continue
# save components
if type_ == Value.NUMBER:
raw.append(item.value.value)
check += 'N'
elif type_ == Value.PERCENTAGE:
if HSL:
# save as percentage fraction
raw.append(item.value.value / 100.0)
else:
# save as real value of percentage of 255
raw.append(int(255 * item.value.value / 100))
check += 'P'
if HSL:
# convert to rgb
# h is 360 based (circle)
h, s, l_ = raw[0] / 360.0, raw[1], raw[2]
# ORDER h l s !!!
r, g, b = colorsys.hls_to_rgb(h, l_, s)
# back to 255 based
rgba = [
int(round(r * 255)),
int(round(g * 255)),
int(round(b * 255)),
]
if len(raw) > 3:
rgba.append(raw[3])
else:
# rgb, rgba
rgba = raw
if len(rgba) < 4:
rgba.append(1.0)
# validate
checks = {
'rgb(': ('NNN', 'PPP'),
'rgba(': ('NNNN', 'PPPN'),
'hsl(': ('NPP',),
'hsla(': ('NPPN',),
}
if check not in checks[functiontype]:
self._log.error(
'ColorValue has invalid %s) parameters: '
'%s (N=Number, P=Percentage)' % (functiontype, check)
)
self._colorType = t
self._red, self._green, self._blue, self._alpha = tuple(rgba)
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_ColorValue(self),
_setCssText,
doc="String value of this value.",
)
value = property(
lambda self: cssutils.ser.do_css_CSSFunction(self, True),
doc='Same as cssText but without comments.',
)
@property
def type(self):
"""Type is fixed to Value.COLOR_VALUE."""
return Value.COLOR_VALUE
def _getName(self):
for n, v in list(self.COLORS.items()):
if v == (self.red, self.green, self.blue, self.alpha):
return n
colorType = property(
lambda self: self._colorType,
doc="IDENT (red), HASH (#f00) or FUNCTION (rgb(255, 0, 0).",
)
name = property(
_getName, doc='Name of the color if known (in ColorValue.COLORS) ' 'else None'
)
red = property(lambda self: self._red, doc='red part as integer between 0 and 255')
@property
def green(self):
"""green part as integer between 0 and 255"""
return self._green
@property
def blue(self):
"""blue part as integer between 0 and 255"""
return self._blue
@property
def alpha(self):
"""alpha part as float between 0.0 and 1.0"""
return self._alpha
class DimensionValue(Value):
"""
A numerical value with an optional dimension like e.g. "px" or "%".
Covers DIMENSION, PERCENTAGE or NUMBER values.
"""
__reUnNumDim = re.compile(r'^([+-]?)(\d*\.\d+|\d+)(.*)$', re.I | re.U | re.X)
_dimension = None
_sign = None
def __str__(self):
return (
""
% (
self.__class__.__name__,
self.type,
self.value,
self.dimension,
self.cssText,
id(self),
)
)
def _setCssText(self, cssText):
self._checkReadonly()
prods = Sequence( # PreDef.unary(),
Choice(
PreDef.dimension(stop=True),
PreDef.number(stop=True),
PreDef.percentage(stop=True),
)
)
ok, seq, store, unused = ProdParser().parse(cssText, 'DimensionValue', prods)
self.wellformed = ok
if ok:
item = seq[0]
sign, v, d = self.__reUnNumDim.findall(normalize(item.value))[0]
if '.' in v:
val = float(sign + v)
else:
val = int(sign + v)
dim = None
if d:
dim = d
self._sign = sign
self._value = val
self._dimension = dim
self._type = item.type
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_Value(self),
_setCssText,
doc="String value of this value including dimension.",
)
dimension = property(
lambda self: self._dimension, # _setValue,
doc="Dimension if a DIMENSION or PERCENTAGE value, " "else None",
)
class URIValue(Value):
"""
An URI value like ``url(example.png)``.
"""
_type = Value.URI
_uri = Value._value
def __str__(self):
return f""
def _setCssText(self, cssText):
self._checkReadonly()
prods = Sequence(PreDef.uri(stop=True))
ok, seq, store, unused = ProdParser().parse(cssText, 'URIValue', prods)
self.wellformed = ok
if ok:
# only 1 value only anyway
self._type = seq[0].type
self._value = seq[0].value
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_Value(self),
_setCssText,
doc='String value of this value.',
)
def _setUri(self, uri):
# TODO: check?
self._value = uri
uri = property(
lambda self: self._value,
_setUri,
doc="Actual URL without delimiters or the empty string",
)
def absoluteUri(self):
"""Actual URL, made absolute if possible, else same as `uri`."""
# Ancestry: PropertyValue, Property, CSSStyleDeclaration, CSSStyleRule,
# CSSStyleSheet
try:
# TODO: better way?
styleSheet = self.parent.parent.parent.parentRule.parentStyleSheet
except AttributeError:
return self.uri
else:
return urllib.parse.urljoin(styleSheet.href, self.uri)
absoluteUri = property(absoluteUri, doc=absoluteUri.__doc__)
class CSSFunction(Value):
"""
A function value.
"""
_functionName = 'Function'
def _productions(self):
"""Return definition used for parsing."""
types = self._prods # rename!
itemProd = Choice(
_ColorProd(self),
_DimensionProd(self),
_URIProd(self),
_ValueProd(self),
_CalcValueProd(self),
_CSSVariableProd(self),
_CSSFunctionProd(self),
)
funcProds = Sequence(
Prod(
name='FUNCTION',
match=lambda t, v: t == types.FUNCTION,
toSeq=lambda t, tokens: (t[0], normalize(t[1])),
),
Choice(
Sequence(
itemProd,
Sequence(
PreDef.comma(optional=True), itemProd, minmax=lambda: (0, None)
),
PreDef.funcEnd(stop=True),
),
PreDef.funcEnd(stop=True),
),
)
return funcProds
def _setCssText(self, cssText):
self._checkReadonly()
ok, seq, store, unused = ProdParser().parse(
cssText, self.type, self._productions()
)
self.wellformed = ok
if ok:
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_CSSFunction(self),
_setCssText,
doc="String value of this value.",
)
value = property(
lambda self: cssutils.ser.do_css_CSSFunction(self, True),
doc='Same as cssText but without comments.',
)
type = property(lambda self: Value.FUNCTION, doc="Type is fixed to Value.FUNCTION.")
class MSValue(CSSFunction):
"""An IE specific Microsoft only function value which is much looser
in what is syntactically allowed."""
_functionName = 'MSValue'
def _productions(self):
"""Return definition used for parsing."""
types = self._prods # rename!
func = Prod(
name='MSValue-Sub',
match=lambda t, v: t == self._prods.FUNCTION,
toSeq=lambda t, tokens: (
MSValue._functionName,
MSValue(pushtoken(t, tokens), parent=self),
),
)
funcProds = Sequence(
Prod(
name='FUNCTION',
match=lambda t, v: t == types.FUNCTION,
toSeq=lambda t, tokens: (t[0], t[1]),
),
Sequence(
Choice(
_ColorProd(self),
_DimensionProd(self),
_URIProd(self),
_ValueProd(self),
_MSValueProd(self),
# _CalcValueProd(self),
_CSSVariableProd(self),
func,
# _CSSFunctionProd(self),
Prod(
name='MSValuePart',
match=lambda t, v: v != ')',
toSeq=lambda t, tokens: (t[0], t[1]),
),
),
minmax=lambda: (0, None),
),
PreDef.funcEnd(stop=True),
)
return funcProds
def _setCssText(self, cssText):
super()._setCssText(cssText)
cssText = property(
lambda self: cssutils.ser.do_css_MSValue(self),
_setCssText,
doc="String value of this value.",
)
class CSSCalc(CSSFunction):
"""The CSSCalc function represents a CSS calc() function.
No further API is provided. For multiplication and division no check
if one operand is a NUMBER is made.
"""
_functionName = 'CSSCalc'
def __str__(self):
return f""
def _setCssText(self, cssText):
self._checkReadonly()
types = self._prods # rename!
_operator = Choice(
Prod(
name='Operator */',
match=lambda t, v: v in '*/',
toSeq=lambda t, tokens: (t[0], t[1]),
),
Sequence(
PreDef.S(),
Choice(
Sequence(
Prod(
name='Operator */',
match=lambda t, v: v in '*/',
toSeq=lambda t, tokens: (t[0], t[1]),
),
PreDef.S(optional=True),
),
Sequence(
Prod(
name='Operator +-',
match=lambda t, v: v in '+-',
toSeq=lambda t, tokens: (t[0], t[1]),
),
PreDef.S(),
),
PreDef.funcEnd(stop=True, mayEnd=True),
),
),
)
_operant = lambda: Choice( # noqa:E731
_DimensionProd(self), _CalcValueProd(self), _CSSVariableProd(self)
)
prods = Sequence(
Prod(
name='CALC',
match=lambda t, v: t == types.FUNCTION and normalize(v) == 'calc(',
),
PreDef.S(optional=True),
_operant(),
Sequence(_operator, _operant(), minmax=lambda: (0, None)),
PreDef.funcEnd(stop=True),
)
# store: name of variable
ok, seq, store, unused = ProdParser().parse(
cssText, 'CSSCalc', prods, checkS=True
)
self.wellformed = ok
if ok:
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_CSSCalc(self),
_setCssText,
doc="String representation of calc function.",
)
type = property(lambda self: Value.CALC, doc="Type is fixed to Value.CALC.")
class CSSVariable(CSSFunction):
"""The CSSVariable represents a CSS variables like ``var(varname)``.
A variable has a (nonnormalized!) `name` and a `value` which is
tried to be resolved from any available CSSVariablesRule definition.
"""
_functionName = 'CSSVariable'
_name = None
_fallback = None
def __str__(self):
return f""
def _setCssText(self, cssText):
self._checkReadonly()
types = self._prods # rename!
prods = Sequence(
Prod(
name='var',
match=lambda t, v: t == types.FUNCTION and normalize(v) == 'var(',
),
PreDef.ident(toStore='ident'),
Sequence(
PreDef.comma(),
Choice(
_ColorProd(self, toStore='fallback'),
_DimensionProd(self, toStore='fallback'),
_URIProd(self, toStore='fallback'),
_ValueProd(self, toStore='fallback'),
_CalcValueProd(self, toStore='fallback'),
_CSSVariableProd(self, toStore='fallback'),
_CSSFunctionProd(self, toStore='fallback'),
),
minmax=lambda: (0, 1),
),
PreDef.funcEnd(stop=True),
)
# store: name of variable
store = {'ident': None, 'fallback': None}
ok, seq, store, unused = ProdParser().parse(cssText, 'CSSVariable', prods)
self.wellformed = ok
if ok:
self._name = store['ident'].value
try:
self._fallback = store['fallback'].value
except KeyError:
self._fallback = None
self._setSeq(seq)
cssText = property(
lambda self: cssutils.ser.do_css_CSSVariable(self),
_setCssText,
doc="String representation of variable.",
)
# TODO: writable? check if var (value) available?
name = property(
lambda self: self._name,
doc="The name identifier of this variable referring to "
"a value in a "
":class:`cssutils.css.CSSVariablesDeclaration`.",
)
@property
def fallback(self):
"""The fallback Value of this variable"""
return self._fallback
type = property(lambda self: Value.VARIABLE, doc="Type is fixed to Value.VARIABLE.")
def _getValue(self):
"Find contained sheet and @variables there"
rel = self
while True:
# find node which has parentRule to get to StyleSheet
if hasattr(rel, 'parent'):
rel = rel.parent
else:
break
try:
variables = rel.parentRule.parentStyleSheet.variables
except AttributeError:
return None
else:
try:
return variables[self.name]
except KeyError:
return None
value = property(_getValue, doc='The resolved actual value or None.')
# helper for productions
def _ValueProd(parent, nextSor=False, toStore=None):
return Prod(
name='Value',
match=lambda t, v: t in ('IDENT', 'STRING', 'UNICODE-RANGE'),
nextSor=nextSor,
toStore=toStore,
toSeq=lambda t, tokens: ('Value', Value(pushtoken(t, tokens), parent=parent)),
)
def _DimensionProd(parent, nextSor=False, toStore=None):
return Prod(
name='Dimension',
match=lambda t, v: t in ('DIMENSION', 'NUMBER', 'PERCENTAGE'),
nextSor=nextSor,
toStore=toStore,
toSeq=lambda t, tokens: (
'DIMENSION',
DimensionValue(pushtoken(t, tokens), parent=parent),
),
)
def _URIProd(parent, nextSor=False, toStore=None):
return Prod(
name='URIValue',
match=lambda t, v: t == 'URI',
toStore=toStore,
nextSor=nextSor,
toSeq=lambda t, tokens: (
'URIValue',
URIValue(pushtoken(t, tokens), parent=parent),
),
)
reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$')
def _ColorProd(parent, nextSor=False, toStore=None):
return Prod(
name='ColorValue',
match=lambda t, v: (t == 'HASH' and reHexcolor.match(v))
or (t == 'FUNCTION' and normalize(v) in ('rgb(', 'rgba(', 'hsl(', 'hsla('))
or (t == 'IDENT' and normalize(v) in list(ColorValue.COLORS.keys())),
nextSor=nextSor,
toStore=toStore,
toSeq=lambda t, tokens: (
'ColorValue',
ColorValue(pushtoken(t, tokens), parent=parent),
),
)
def _CSSFunctionProd(parent, nextSor=False, toStore=None):
return PreDef.function(
nextSor=nextSor,
toStore=toStore,
toSeq=lambda t, tokens: (
CSSFunction._functionName,
CSSFunction(pushtoken(t, tokens), parent=parent),
),
)
def _CalcValueProd(parent, nextSor=False, toStore=None):
return Prod(
name=CSSCalc._functionName,
match=lambda t, v: t == PreDef.types.FUNCTION and normalize(v) == 'calc(',
toStore=toStore,
toSeq=lambda t, tokens: (
CSSCalc._functionName,
CSSCalc(pushtoken(t, tokens), parent=parent),
),
nextSor=nextSor,
)
def _CSSVariableProd(parent, nextSor=False, toStore=None):
return PreDef.variable(
nextSor=nextSor,
toStore=toStore,
toSeq=lambda t, tokens: (
CSSVariable._functionName,
CSSVariable(pushtoken(t, tokens), parent=parent),
),
)
def _MSValueProd(parent, nextSor=False):
return Prod(
name=MSValue._functionName,
match=lambda t, v: ( # t == self._prods.FUNCTION and (
normalize(v)
in (
'expression(',
'alpha(',
'blur(',
'chroma(',
'dropshadow(',
'fliph(',
'flipv(',
'glow(',
'gray(',
'invert(',
'mask(',
'shadow(',
'wave(',
'xray(',
)
or v.startswith('progid:DXImageTransform.Microsoft.')
),
nextSor=nextSor,
toSeq=lambda t, tokens: (
MSValue._functionName,
MSValue(pushtoken(t, tokens), parent=parent),
),
)
def MediaQueryValueProd(parent):
return Choice(
_ColorProd(parent),
_DimensionProd(parent),
_ValueProd(parent),
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/css2productions.py 0000644 0001751 0000177 00000013622 14627633762 020465 0 ustar 00runner docker """productions for CSS 2.1
CSS2_1_MACROS and CSS2_1_PRODUCTIONS are from both
http://www.w3.org/TR/CSS21/grammar.html and
http://www.w3.org/TR/css3-syntax/#grammar0
"""
__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS']
# option case-insensitive
MACROS = {
'h': r'[0-9a-f]',
# 'nonascii': r'[\200-\377]',
'nonascii': r'[^\0-\177]', # CSS3
'unicode': r'\\{h}{1,6}(\r\n|[ \t\r\n\f])?',
'escape': r'{unicode}|\\[^\r\n\f0-9a-f]',
'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}',
'nmchar': r'[_a-zA-Z0-9-]|{nonascii}|{escape}',
'string1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*\"',
'string2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*\'",
'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*',
'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*",
'comment': r'\/\*[^*]*\*+([^/*][^*]*\*+)*\/',
# CSS list 080725 19:43
# \/\*([^*\\]|{escape})*\*+(([^/*\\]|{escape})[^*]*\*+)*\/
'ident': r'[-]?{nmstart}{nmchar}*',
'name': r'{nmchar}+',
# CHANGED TO SPEC: added "-?"
'num': r'-?[0-9]*\.[0-9]+|[0-9]+',
'string': r'{string1}|{string2}',
'invalid': r'{invalid1}|{invalid2}',
'url': r'([!#$%&*-~]|{nonascii}|{escape})*',
's': r'[ \t\r\n\f]+',
'w': r'{s}?',
'nl': r'\n|\r\n|\r|\f',
'range': r'\?{1,6}|{h}(\?{0,5}|{h}(\?{0,4}|{h}(\?{0,3}|{h}(\?{0,2}|{h}(\??|{h})))))',
'A': r'a|\\0{0,4}(41|61)(\r\n|[ \t\r\n\f])?',
'C': r'c|\\0{0,4}(43|63)(\r\n|[ \t\r\n\f])?',
'D': r'd|\\0{0,4}(44|64)(\r\n|[ \t\r\n\f])?',
'E': r'e|\\0{0,4}(45|65)(\r\n|[ \t\r\n\f])?',
'F': r'f|\\0{0,4}(46|66)(\r\n|[ \t\r\n\f])?',
'G': r'g|\\0{0,4}(47|67)(\r\n|[ \t\r\n\f])?|\\g',
'H': r'h|\\0{0,4}(48|68)(\r\n|[ \t\r\n\f])?|\\h',
'I': r'i|\\0{0,4}(49|69)(\r\n|[ \t\r\n\f])?|\\i',
'K': r'k|\\0{0,4}(4b|6b)(\r\n|[ \t\r\n\f])?|\\k',
'M': r'm|\\0{0,4}(4d|6d)(\r\n|[ \t\r\n\f])?|\\m',
'N': r'n|\\0{0,4}(4e|6e)(\r\n|[ \t\r\n\f])?|\\n',
'O': r'o|\\0{0,4}(51|71)(\r\n|[ \t\r\n\f])?|\\o',
'P': r'p|\\0{0,4}(50|70)(\r\n|[ \t\r\n\f])?|\\p',
'R': r'r|\\0{0,4}(52|72)(\r\n|[ \t\r\n\f])?|\\r',
'S': r's|\\0{0,4}(53|73)(\r\n|[ \t\r\n\f])?|\\s',
'T': r't|\\0{0,4}(54|74)(\r\n|[ \t\r\n\f])?|\\t',
'X': r'x|\\0{0,4}(58|78)(\r\n|[ \t\r\n\f])?|\\x',
'Z': r'z|\\0{0,4}(5a|7a)(\r\n|[ \t\r\n\f])?|\\z',
}
PRODUCTIONS = [
('URI', r'url\({w}{string}{w}\)'), # "url("{w}{string}{w}")" {return URI;}
('URI', r'url\({w}{url}{w}\)'), # "url("{w}{url}{w}")" {return URI;}
('FUNCTION', r'{ident}\('), # {ident}"(" {return FUNCTION;}
('IMPORT_SYM', r'@{I}{M}{P}{O}{R}{T}'), # "@import" {return IMPORT_SYM;}
('PAGE_SYM', r'@{P}{A}{G}{E}'), # "@page" {return PAGE_SYM;}
('MEDIA_SYM', r'@{M}{E}{D}{I}{A}'), # "@media" {return MEDIA_SYM;}
(
'FONT_FACE_SYM',
r'@{F}{O}{N}{T}\-{F}{A}{C}{E}',
), # "@font-face" {return FONT_FACE_SYM;}
# CHANGED TO SPEC: only @charset
('CHARSET_SYM', r'@charset '), # "@charset " {return CHARSET_SYM;}
(
'NAMESPACE_SYM',
r'@{N}{A}{M}{E}{S}{P}{A}{C}{E}',
), # "@namespace" {return NAMESPACE_SYM;}
# CHANGED TO SPEC: ATKEYWORD
('ATKEYWORD', r'\@{ident}'),
('IDENT', r'{ident}'), # {ident} {return IDENT;}
('STRING', r'{string}'), # {string} {return STRING;}
('INVALID', r'{invalid}'), # {return INVALID; /* unclosed string */}
('HASH', r'\#{name}'), # "#"{name} {return HASH;}
('PERCENTAGE', r'{num}%'), # {num}% {return PERCENTAGE;}
('LENGTH', r'{num}{E}{M}'), # {num}em {return EMS;}
('LENGTH', r'{num}{E}{X}'), # {num}ex {return EXS;}
('LENGTH', r'{num}{P}{X}'), # {num}px {return LENGTH;}
('LENGTH', r'{num}{C}{M}'), # {num}cm {return LENGTH;}
('LENGTH', r'{num}{M}{M}'), # {num}mm {return LENGTH;}
('LENGTH', r'{num}{I}{N}'), # {num}in {return LENGTH;}
('LENGTH', r'{num}{P}{T}'), # {num}pt {return LENGTH;}
('LENGTH', r'{num}{P}{C}'), # {num}pc {return LENGTH;}
('ANGLE', r'{num}{D}{E}{G}'), # {num}deg {return ANGLE;}
('ANGLE', r'{num}{R}{A}{D}'), # {num}rad {return ANGLE;}
('ANGLE', r'{num}{G}{R}{A}{D}'), # {num}grad {return ANGLE;}
('TIME', r'{num}{M}{S}'), # {num}ms {return TIME;}
('TIME', r'{num}{S}'), # {num}s {return TIME;}
('FREQ', r'{num}{H}{Z}'), # {num}Hz {return FREQ;}
('FREQ', r'{num}{K}{H}{Z}'), # {num}kHz {return FREQ;}
('DIMEN', r'{num}{ident}'), # {num}{ident} {return DIMEN;}
('NUMBER', r'{num}'), # {num} {return NUMBER;}
# ('UNICODERANGE', r'U\+{range}'), #U\+{range} {return UNICODERANGE;}
# ('UNICODERANGE', r'U\+{h}{1,6}-{h}{1,6}'), #U\+{h}{1,6}-{h}{1,6} {return UNICODERANGE;} # noqa
# --- CSS3 ---
('UNICODE-RANGE', r'[0-9A-F?]{1,6}(\-[0-9A-F]{1,6})?'),
('CDO', r'\<\!\-\-'), # "" {return CDC;}
('S', r'{s}'), # {return S;}
# \/\*[^*]*\*+([^/*][^*]*\*+)*\/ /* ignore comments */
# {s}+\/\*[^*]*\*+([^/*][^*]*\*+)*\/ {unput(' '); /*replace by space*/}
('INCLUDES', r'\~\='), # "~=" {return INCLUDES;}
('DASHMATCH', r'\|\='), # "|=" {return DASHMATCH;}
('LBRACE', r'\{'), # {w}"{" {return LBRACE;}
('PLUS', r'\+'), # {w}"+" {return PLUS;}
('GREATER', r'\>'), # {w}">" {return GREATER;}
('COMMA', r'\,'), # {w}"," {return COMMA;}
(
'IMPORTANT_SYM',
r'\!({w}|{comment})*{I}{M}{P}{O}{R}{T}{A}{N}{T}',
), # "!{w}important" {return IMPORTANT_SYM;}
('COMMENT', r'\/\*[^*]*\*+([^/][^*]*\*+)*\/'), # /* ignore comments */
('CLASS', r'\.'), # . {return *yytext;}
# --- CSS3! ---
('CHAR', r'[^"\']'),
]
class CSSProductions:
pass
for t in PRODUCTIONS:
setattr(CSSProductions, t[0].replace('-', '_'), t[0])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/cssproductions.py 0000644 0001751 0000177 00000011072 14627633762 020400 0 ustar 00runner docker """productions for cssutils based on a mix of CSS 2.1 and CSS 3 Syntax
productions
- http://www.w3.org/TR/css3-syntax
- http://www.w3.org/TR/css3-syntax/#grammar0
open issues
- numbers contain "-" if present
- HASH: #aaa is, #000 is not anymore,
CSS2.1: 'nmchar': r'[_a-z0-9-]|{nonascii}|{escape}',
CSS3: 'nmchar': r'[_a-z-]|{nonascii}|{escape}',
"""
__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS']
# a complete list of css3 macros
MACROS = {
'nonascii': r'[^\0-\177]',
'unicode': r'\\[0-9A-Fa-f]{1,6}(?:{nl}|{s})?',
# 'escape': r'{unicode}|\\[ -~\200-\777]',
'escape': r'{unicode}|\\[^\n\r\f0-9a-f]',
'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}',
'nmchar': r'[-_a-zA-Z0-9]|{nonascii}|{escape}',
'string1': r'"([^\n\r\f\\"]|\\{nl}|{escape})*"',
'string2': r"'([^\n\r\f\\']|\\{nl}|{escape})*'",
'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*',
'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*",
'comment': r'\/\*[^*]*\*+([^/][^*]*\*+)*\/',
'ident': r'[-]{0,2}{nmstart}{nmchar}*',
'name': r'{nmchar}+',
# TODO???
'num': r'[+-]?[0-9]*\.[0-9]+|[+-]?[0-9]+', # r'[-]?\d+|[-]?\d*\.\d+',
'string': r'{string1}|{string2}',
# from CSS2.1
'invalid': r'{invalid1}|{invalid2}',
'url': r'[\x09\x21\x23-\x26\x28\x2a-\x7E]|{nonascii}|{escape}',
's': r'\t|\r|\n|\f|\x20',
'w': r'{s}*',
'nl': r'\n|\r\n|\r|\f',
'A': r'A|a|\\0{0,4}(?:41|61)(?:\r\n|[ \t\r\n\f])?',
'B': r'B|b|\\0{0,4}(?:42|62)(?:\r\n|[ \t\r\n\f])?',
'C': r'C|c|\\0{0,4}(?:43|63)(?:\r\n|[ \t\r\n\f])?',
'D': r'D|d|\\0{0,4}(?:44|64)(?:\r\n|[ \t\r\n\f])?',
'E': r'E|e|\\0{0,4}(?:45|65)(?:\r\n|[ \t\r\n\f])?',
'F': r'F|f|\\0{0,4}(?:46|66)(?:\r\n|[ \t\r\n\f])?',
'G': r'G|g|\\0{0,4}(?:47|67)(?:\r\n|[ \t\r\n\f])?|\\G|\\g',
'H': r'H|h|\\0{0,4}(?:48|68)(?:\r\n|[ \t\r\n\f])?|\\H|\\h',
'I': r'I|i|\\0{0,4}(?:49|69)(?:\r\n|[ \t\r\n\f])?|\\I|\\i',
'K': r'K|k|\\0{0,4}(?:4b|6b)(?:\r\n|[ \t\r\n\f])?|\\K|\\k',
'L': r'L|l|\\0{0,4}(?:4c|6c)(?:\r\n|[ \t\r\n\f])?|\\L|\\l',
'M': r'M|m|\\0{0,4}(?:4d|6d)(?:\r\n|[ \t\r\n\f])?|\\M|\\m',
'N': r'N|n|\\0{0,4}(?:4e|6e)(?:\r\n|[ \t\r\n\f])?|\\N|\\n',
'O': r'O|o|\\0{0,4}(?:4f|6f)(?:\r\n|[ \t\r\n\f])?|\\O|\\o',
'P': r'P|p|\\0{0,4}(?:50|70)(?:\r\n|[ \t\r\n\f])?|\\P|\\p',
'R': r'R|r|\\0{0,4}(?:52|72)(?:\r\n|[ \t\r\n\f])?|\\R|\\r',
'S': r'S|s|\\0{0,4}(?:53|73)(?:\r\n|[ \t\r\n\f])?|\\S|\\s',
'T': r'T|t|\\0{0,4}(?:54|74)(?:\r\n|[ \t\r\n\f])?|\\T|\\t',
'U': r'U|u|\\0{0,4}(?:55|75)(?:\r\n|[ \t\r\n\f])?|\\U|\\u',
'V': r'V|v|\\0{0,4}(?:56|76)(?:\r\n|[ \t\r\n\f])?|\\V|\\v',
'X': r'X|x|\\0{0,4}(?:58|78)(?:\r\n|[ \t\r\n\f])?|\\X|\\x',
'Z': r'Z|z|\\0{0,4}(?:5a|7a)(?:\r\n|[ \t\r\n\f])?|\\Z|\\z',
}
# The following productions are the complete list of tokens
# used by cssutils, a mix of CSS3 and some CSS2.1 productions.
# The productions are **ordered**:
PRODUCTIONS = [
# UTF8_BOM or UTF8_BOM_SIG will only be checked at beginning of CSS
('BOM', '\xfe\xff|\xef\xbb\xbf'),
('S', r'{s}+'), # 1st in list of general productions
('URI', r'{U}{R}{L}\({w}({string}|{url}*){w}\)'),
('UNICODE-RANGE', r'{U}\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?'),
('IDENT', r'{ident}'),
('FUNCTION', r'{ident}\('),
('DIMENSION', r'{num}{ident}'),
('PERCENTAGE', r'{num}\%'),
('NUMBER', r'{num}'),
('HASH', r'\#{name}'),
('COMMENT', r'{comment}'), # r'\/\*[^*]*\*+([^/][^*]*\*+)*\/'),
('STRING', r'{string}'),
('INVALID', r'{invalid}'), # from CSS2.1
('ATKEYWORD', r'@{ident}'), # other keywords are done in the tokenizer
('INCLUDES', r'\~\='),
('DASHMATCH', r'\|\='),
('PREFIXMATCH', r'\^\='),
('SUFFIXMATCH', r'\$\='),
('SUBSTRINGMATCH', r'\*\='),
('CDO', r'\<\!\-\-'),
('CDC', r'\-\-\>'),
('CHAR', r'[^"\']'), # MUST always be last
# valid ony at start so not checked everytime
# ('CHARSET_SYM', r'@charset '), # from Errata includes ending space!
# checked specially if fullsheet is parsed
]
class CSSProductions:
"""
most attributes are set later
"""
EOF = True
# removed from productions as they simply are ATKEYWORD until
# tokenizing
CHARSET_SYM = 'CHARSET_SYM'
FONT_FACE_SYM = 'FONT_FACE_SYM'
MEDIA_SYM = 'MEDIA_SYM'
IMPORT_SYM = 'IMPORT_SYM'
NAMESPACE_SYM = 'NAMESPACE_SYM'
PAGE_SYM = 'PAGE_SYM'
VARIABLES_SYM = 'VARIABLES_SYM'
for t in PRODUCTIONS:
setattr(CSSProductions, t[0].replace('-', '_'), t[0])
# may be enabled by settings.set
_DXImageTransform = ('FUNCTION', r'progid\:DXImageTransform\.Microsoft\..+\(')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/errorhandler.py 0000644 0001751 0000177 00000007271 14627633762 020013 0 ustar 00runner docker """cssutils ErrorHandler
ErrorHandler
used as log with usual levels (debug, info, warn, error)
if instanciated with ``raiseExceptions=True`` raises exeptions instead
of logging
log
defaults to instance of ErrorHandler for any kind of log message from
lexerm, parser etc.
- raiseExceptions = [False, True]
- setloglevel(loglevel)
"""
__all__ = ['ErrorHandler']
import logging
import urllib.error
import urllib.parse
import urllib.request
import xml.dom
class _ErrorHandler:
"""
handles all errors and log messages
"""
def __init__(self, log, defaultloglevel=logging.INFO, raiseExceptions=True):
"""
inits log if none given
log
for parse messages, default logs to sys.stderr
defaultloglevel
if none give this is logging.DEBUG
raiseExceptions
- True: Errors will be raised e.g. during building
- False: Errors will be written to the log, this is the
default behaviour when parsing
"""
# may be disabled during setting of known valid items
self.enabled = True
if log:
self._log = log
else:
import sys
self._log = logging.getLogger('CSSUTILS')
hdlr = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('%(levelname)s\t%(message)s')
hdlr.setFormatter(formatter)
self._log.addHandler(hdlr)
self._log.setLevel(defaultloglevel)
self.raiseExceptions = raiseExceptions
def __getattr__(self, name):
"use self._log items"
calls = ('debug', 'info', 'warn', 'error', 'critical', 'fatal')
other = ('setLevel', 'getEffectiveLevel', 'addHandler', 'removeHandler')
if name in calls:
if name == 'warn':
name = 'warning'
self._logcall = getattr(self._log, name)
return self.__handle
elif name in other:
return getattr(self._log, name)
else:
raise AttributeError('(errorhandler) No Attribute %r found' % name)
def __handle(
self, msg='', token=None, error=xml.dom.SyntaxErr, neverraise=False, args=None
):
"""
handles all calls
logs or raises exception
"""
if self.enabled:
if error is None:
error = xml.dom.SyntaxErr
line, col = None, None
if token:
if isinstance(token, tuple):
value, line, col = token[1], token[2], token[3]
else:
value, line, col = token.value, token.line, token.col
msg = f'{msg} [{line}:{col}: {value}]'
if error and self.raiseExceptions and not neverraise:
if isinstance(error, urllib.error.HTTPError) or isinstance(
error, urllib.error.URLError
):
raise
elif issubclass(error, xml.dom.DOMException):
error.line = line
error.col = col
raise error(msg)
else:
self._logcall(msg)
def setLog(self, log):
"""set log of errorhandler's log"""
self._log = log
class ErrorHandler(_ErrorHandler):
"Singleton, see _ErrorHandler"
instance = None
def __init__(self, log=None, defaultloglevel=logging.INFO, raiseExceptions=True):
if ErrorHandler.instance is None:
ErrorHandler.instance = _ErrorHandler(
log=log,
defaultloglevel=defaultloglevel,
raiseExceptions=raiseExceptions,
)
self.__dict__ = ErrorHandler.instance.__dict__
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/helper.py 0000644 0001751 0000177 00000006157 14627633762 016605 0 ustar 00runner docker """cssutils helper TEST"""
import itertools
import os
import re
import urllib.error
import urllib.parse
import urllib.request
class Deprecated:
"""This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
It accepts a single paramter ``msg`` which is shown with the warning.
It should contain information which function or method to use instead.
"""
def __init__(self, msg):
self.msg = msg
def __call__(self, func):
def newFunc(*args, **kwargs):
import warnings
warnings.warn(
f"Call to deprecated method {func.__name__!r}. {self.msg}",
category=DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
newFunc.__name__ = func.__name__
newFunc.__doc__ = func.__doc__
newFunc.__dict__.update(func.__dict__)
return newFunc
# simple escapes, all non unicodes
_simpleescapes = re.compile(r'(\\[^0-9a-fA-F])').sub
def normalize(x):
r"""
normalizes x, namely:
- remove any \ before non unicode sequences (0-9a-zA-Z) so for
x==r"c\olor\" return "color" (unicode escape sequences should have
been resolved by the tokenizer already)
- lowercase
"""
if x:
def removeescape(matchobj):
return matchobj.group(0)[1:]
x = _simpleescapes(removeescape, x)
return x.lower()
else:
return x
def path2url(path):
"""Return file URL of `path`"""
return 'file:' + urllib.request.pathname2url(os.path.abspath(path))
def pushtoken(token, tokens):
"""Return new generator starting with token followed by all tokens in
``tokens``"""
return itertools.chain([token], tokens)
def string(value):
"""
Serialize value with quotes e.g.::
``a \'string`` => ``'a \'string'``
"""
# \n = 0xa, \r = 0xd, \f = 0xc
value = (
value.replace('\n', '\\a ')
.replace('\r', '\\d ')
.replace('\f', '\\c ')
.replace('"', '\\"')
)
if value.endswith('\\'):
value = value[:-1] + '\\\\'
return '"%s"' % value
def stringvalue(string):
"""
Retrieve actual value of string without quotes. Escaped
quotes inside the value are resolved, e.g.::
``'a \'string'`` => ``a 'string``
"""
return string.replace('\\' + string[0], string[0])[1:-1]
_match_forbidden_in_uri = re.compile(r'''.*?[\(\)\s\;,'"]''', re.U).match
def uri(value):
"""
Serialize value by adding ``url()`` and with quotes if needed e.g.::
``"`` => ``url("\"")``
"""
if _match_forbidden_in_uri(value):
value = string(value)
return 'url(%s)' % value
def urivalue(uri):
"""
Return actual content without surrounding "url(" and ")"
and removed surrounding quotes too including contained
escapes of quotes, e.g.::
``url("\"")`` => ``"``
"""
uri = uri[uri.find('(') + 1 : -1].strip()
if uri and (uri[0] in '\'"') and (uri[0] == uri[-1]):
return stringvalue(uri)
else:
return uri
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/parse.py 0000644 0001751 0000177 00000020552 14627633762 016433 0 ustar 00runner docker """A validating CSSParser"""
__all__ = ['CSSParser']
import codecs
import cssutils
from cssutils import css
from . import tokenize2
from .helper import path2url
class CSSParser:
"""Parse a CSS StyleSheet from URL, string or file and return a DOM Level 2
CSS StyleSheet object.
Usage::
parser = CSSParser()
# optionally
parser.setFetcher(fetcher)
sheet = parser.parseFile('test1.css', 'ascii')
print sheet.cssText
"""
def __init__(
self,
log=None,
loglevel=None,
raiseExceptions=None,
fetcher=None,
parseComments=True,
validate=True,
):
"""
:param log:
logging object
:param loglevel:
logging loglevel
:param raiseExceptions:
if log should simply log (default) or raise errors during
parsing. Later while working with the resulting sheets
the setting used in cssutils.log.raiseExeptions is used
:param fetcher:
see ``setFetcher(fetcher)``
:param parseComments:
if comments should be added to CSS DOM or simply omitted
:param validate:
if parsing should validate, may be overwritten in parse methods
"""
if log is not None:
cssutils.log.setLog(log)
if loglevel is not None:
cssutils.log.setLevel(loglevel)
# remember global setting
self.__globalRaising = cssutils.log.raiseExceptions
if raiseExceptions:
self.__parseRaising = raiseExceptions
else:
# DEFAULT during parse
self.__parseRaising = False
self.__tokenizer = tokenize2.Tokenizer(doComments=parseComments)
self.setFetcher(fetcher)
self._validate = validate
def __parseSetting(self, parse):
"""during parse exceptions may be handled differently depending on
init parameter ``raiseExceptions``
"""
if parse:
cssutils.log.raiseExceptions = self.__parseRaising
else:
cssutils.log.raiseExceptions = self.__globalRaising
def parseStyle(self, cssText, encoding='utf-8', validate=None):
"""Parse given `cssText` which is assumed to be the content of
a HTML style attribute.
:param cssText:
CSS string to parse
:param encoding:
It will be used to decode `cssText` if given as a (byte)
string.
:param validate:
If given defines if validation is used. Uses CSSParser settings as
fallback
:returns:
:class:`~cssutils.css.CSSStyleDeclaration`
"""
self.__parseSetting(True)
if isinstance(cssText, bytes):
# TODO: use codecs.getdecoder('css') here?
cssText = cssText.decode(encoding)
if validate is None:
validate = self._validate
style = css.CSSStyleDeclaration(cssText, validating=validate)
self.__parseSetting(False)
return style
def parseString(
self, cssText, encoding=None, href=None, media=None, title=None, validate=None
):
"""Parse `cssText` as :class:`~cssutils.css.CSSStyleSheet`.
Errors may be raised (e.g. UnicodeDecodeError).
:param cssText:
CSS string to parse
:param encoding:
If ``None`` the encoding will be read from BOM or an @charset
rule or defaults to UTF-8.
If given overrides any found encoding including the ones for
imported sheets.
It also will be used to decode `cssText` if given as a (byte)
string.
:param href:
The ``href`` attribute to assign to the parsed style sheet.
Used to resolve other urls in the parsed sheet like @import hrefs.
:param media:
The ``media`` attribute to assign to the parsed style sheet
(may be a MediaList, list or a string).
:param title:
The ``title`` attribute to assign to the parsed style sheet.
:param validate:
If given defines if validation is used. Uses CSSParser settings as
fallback
:returns:
:class:`~cssutils.css.CSSStyleSheet`.
"""
self.__parseSetting(True)
# TODO: py3 needs bytes here!
if isinstance(cssText, bytes):
cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0]
if validate is None:
validate = self._validate
sheet = cssutils.css.CSSStyleSheet(
href=href,
media=cssutils.stylesheets.MediaList(media),
title=title,
validating=validate,
)
sheet._setFetcher(self.__fetcher)
# tokenizing this ways closes open constructs and adds EOF
sheet._setCssTextWithEncodingOverride(
self.__tokenizer.tokenize(cssText, fullsheet=True),
encodingOverride=encoding,
)
self.__parseSetting(False)
return sheet
def parseFile(
self, filename, encoding=None, href=None, media=None, title=None, validate=None
):
"""Retrieve content from `filename` and parse it. Errors may be raised
(e.g. IOError).
:param filename:
of the CSS file to parse, if no `href` is given filename is
converted to a (file:) URL and set as ``href`` of resulting
stylesheet.
If `href` is given it is set as ``sheet.href``. Either way
``sheet.href`` is used to resolve e.g. stylesheet imports via
@import rules.
:param encoding:
Value ``None`` defaults to encoding detection via BOM or an
@charset rule.
Other values override detected encoding for the sheet at
`filename` including any imported sheets.
:returns:
:class:`~cssutils.css.CSSStyleSheet`.
"""
if not href:
# prepend // for file URL, urllib does not do this?
# href = u'file:' + urllib.pathname2url(os.path.abspath(filename))
href = path2url(filename)
with open(filename, 'rb') as fd:
css = fd.read()
return self.parseString(
css,
encoding=encoding, # read returns a str
href=href,
media=media,
title=title,
validate=validate,
)
def parseUrl(self, href, encoding=None, media=None, title=None, validate=None):
"""Retrieve content from URL `href` and parse it. Errors may be raised
(e.g. URLError).
:param href:
URL of the CSS file to parse, will also be set as ``href`` of
resulting stylesheet
:param encoding:
Value ``None`` defaults to encoding detection via HTTP, BOM or an
@charset rule.
A value overrides detected encoding for the sheet at ``href``
including any imported sheets.
:returns:
:class:`~cssutils.css.CSSStyleSheet`.
"""
encoding, enctype, text = cssutils.util._readUrl(
href, fetcher=self.__fetcher, overrideEncoding=encoding
)
if enctype == 5:
# do not use if defaulting to UTF-8
encoding = None
if text is not None:
return self.parseString(
text,
encoding=encoding,
href=href,
media=media,
title=title,
validate=validate,
)
def setFetcher(self, fetcher=None):
"""Replace the default URL fetch function with a custom one.
:param fetcher:
A function which gets a single parameter
``url``
the URL to read
and must return ``(encoding, content)`` where ``encoding`` is the
HTTP charset normally given via the Content-Type header (which may
simply omit the charset in which case ``encoding`` would be
``None``) and ``content`` being the string (or unicode) content.
The Mimetype should be 'text/css' but this has to be checked by the
fetcher itself (the default fetcher emits a warning if encountering
a different mimetype).
Calling ``setFetcher`` with ``fetcher=None`` resets cssutils
to use its default function.
"""
self.__fetcher = fetcher
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/prodparser.py 0000644 0001751 0000177 00000065716 14627633762 017515 0 ustar 00runner docker """Productions parser used by css and stylesheets classes to parse
test into a cssutils.util.Seq and at the same time retrieving
additional specific cssutils.util.Item objects for later use.
TODO:
- ProdsParser
- handle EOF or STOP?
- handle unknown @rules
- handle S: maybe save to Seq? parameterized?
- store['_raw']: always?
- Sequence:
- opt first(), naive impl for now
"""
__all__ = ['ProdParser', 'Sequence', 'Choice', 'Prod', 'PreDef']
import itertools
import re
import sys
import types
import cssutils
from .helper import pushtoken
class ParseError(Exception):
"""Base Exception class for ProdParser (used internally)."""
pass
class Done(ParseError):
"""Raised if Sequence or Choice is finished and no more Prods left."""
pass
class Exhausted(ParseError):
"""Raised if Sequence or Choice is finished but token is given."""
pass
class Missing(ParseError):
"""Raised if Sequence or Choice is not finished but no matching token given."""
pass
class NoMatch(ParseError):
"""Raised if nothing in Sequence or Choice does match."""
pass
class Choice:
"""A Choice of productions (Sequence or single Prod)."""
def __init__(self, *prods, **options):
"""
*prods
Prod or Sequence objects
options:
optional=False
"""
self._prods = prods
try:
self.optional = options['optional']
except KeyError:
for p in self._prods:
if p.optional:
self.optional = True
break
else:
self.optional = False
self.reset()
def reset(self):
"""Start Choice from zero"""
self._exhausted = False
def matches(self, token):
"""Check if token matches"""
for prod in self._prods:
if prod.matches(token):
return True
return False
def nextProd(self, token):
"""
Return:
- next matching Prod or Sequence
- ``None`` if any Prod or Sequence is optional and no token matched
- raise ParseError if nothing matches and all are mandatory
- raise Exhausted if choice already done
``token`` may be None but this occurs when no tokens left."""
# print u'TEST for %s in %s' % (token, self)
if not self._exhausted:
optional = False
for p in self._prods:
if p.matches(token):
self._exhausted = True
p.reset()
# print u'FOUND for %s: %s' % (token, p);#print
return p
elif p.optional:
optional = True
else:
if not optional:
# None matched but also None is optional
raise NoMatch(f'No match for {token} in {self}')
# raise ParseError(u'No match in %s for %s' % (self, token))
elif token:
raise Exhausted('Extra token')
def __repr__(self):
return f""
def __str__(self):
return 'Choice(%s)' % ', '.join([str(x) for x in self._prods])
class Sequence:
"""A Sequence of productions (Choice or single Prod)."""
def __init__(self, *prods, **options):
"""
*prods
Prod or Choice or Sequence objects
**options:
minmax = lambda: (1, 1)
callback returning number of times this sequence may run
"""
self._prods = prods
try:
minmax = options['minmax']
except KeyError:
minmax = lambda: (1, 1) # noqa: E731
self._min, self._max = minmax()
if self._max is None:
# unlimited
try:
# py2.6/3
self._max = sys.maxsize
except AttributeError:
# py<2.6
self._max = sys.maxsize
self._prodcount = len(self._prods)
self.reset()
def matches(self, token):
"""Called by Choice to try to find if Sequence matches."""
for prod in self._prods:
if prod.matches(token):
return True
try:
if not prod.optional:
break
except AttributeError:
pass
return False
def reset(self):
"""Reset this Sequence if it is nested."""
self._roundstarted = False
self._i = 0
self._round = 0
def _currentName(self):
"""Return current element of Sequence, used by name"""
# TODO: current impl first only if 1st if an prod!
for prod in self._prods[self._i :]:
if not prod.optional:
return str(prod)
else:
return 'Sequence'
optional = property(lambda self: self._min == 0)
def nextProd(self, token):
"""Return
- next matching Prod or Choice
- raises ParseError if nothing matches
- raises Exhausted if sequence already done
"""
# print u'TEST for %s in %s' % (token, self)
while self._round < self._max:
# for this round
i = self._i
round = self._round
p = self._prods[i]
if i == 0:
self._roundstarted = False
# for next round
self._i += 1
if self._i == self._prodcount:
self._round += 1
self._i = 0
if p.matches(token):
self._roundstarted = True
# reset nested Choice or Prod to use from start
p.reset()
# print u'FOUND for %s: %s' % (token, p);#print
return p
elif p.optional:
continue
elif (
round < self._min or self._roundstarted
): # or (round == 0 and self._min == 0):
raise Missing('Missing token for production %s' % p)
elif not token:
if self._roundstarted:
raise Missing('Missing token for production %s' % p)
else:
raise Done()
else:
raise NoMatch(f'No match for {token} in {self}')
if token:
raise Exhausted('Extra token')
def __repr__(self):
return f""
def __str__(self):
return 'Sequence(%s)' % ', '.join([str(x) for x in self._prods])
class Prod:
"""Single Prod in Sequence or Choice."""
def __init__(
self,
name,
match,
optional=False,
toSeq=None,
toStore=None,
stop=False,
stopAndKeep=False,
stopIfNoMoreMatch=False,
nextSor=False,
mayEnd=False,
storeToken=None,
exception=None,
):
"""
name
name used for error reporting
match callback
function called with parameters tokentype and tokenvalue
returning True, False or raising ParseError
toSeq callback (optional) or False
calling toSeq(token, tokens) returns (type_, val) == (token[0], token[1])
to be appended to seq else simply unaltered (type_, val)
if False nothing is added
toStore (optional)
key to save util.Item to store or callback(store, util.Item)
optional = False
whether Prod is optional or not
stop = False
if True stop parsing of tokens here
stopAndKeep
if True stop parsing of tokens here but return stopping
token in unused tokens
stopIfNoMoreMatch = False
stop even if more tokens available, similar to stop and keep but with
condition no more matches
nextSor=False
next is S or other like , or / (CSSValue)
mayEnd = False
no token must follow even defined by Sequence.
Used for operator ',/ ' currently only
storeToken = None
if True toStore saves simple token tuple and not and Item object
to store. Old style processing, TODO: resolve
exception = None
exception to be raised in case of error, normaly SyntaxErr
"""
self._name = name
self.match = match
self.optional = optional
self.stop = stop
self.stopAndKeep = stopAndKeep
self.stopIfNoMoreMatch = stopIfNoMoreMatch
self.nextSor = nextSor
self.mayEnd = mayEnd
self.storeToken = storeToken
self.exception = exception
def makeToStore(key):
"Return a function used by toStore."
def toStore(store, item):
"Set or append store item."
if key in store:
_v = store[key]
if not isinstance(_v, list):
store[key] = [_v]
store[key].append(item)
else:
store[key] = item
return toStore
if toSeq or toSeq is False:
# called: seq.append(toSeq(value))
self.toSeq = toSeq
else:
self.toSeq = lambda t, tokens: (t[0], t[1])
if callable(toStore):
self.toStore = toStore
elif toStore:
self.toStore = makeToStore(toStore)
else:
# always set!
self.toStore = None
def matches(self, token):
"""Return if token matches."""
if not token:
return False
type_, val, line, col = token
return self.match(type_, val)
def reset(self):
pass
def __str__(self):
return self._name
def __repr__(self):
return f""
# global tokenizer as there is only one!
tokenizer = cssutils.tokenize2.Tokenizer()
# global: saved from subProds
savedTokens = []
class ProdParser:
"""Productions parser."""
def __init__(self, clear=True):
self.types = cssutils.cssproductions.CSSProductions
self._log = cssutils.log
if clear:
tokenizer.clear()
def _texttotokens(self, text):
"""Build a generator which is the only thing that is parsed!
old classes may use lists etc
"""
if isinstance(text, str):
# DEFAULT, to tokenize strip space
return tokenizer.tokenize(text.strip())
elif isinstance(text, types.GeneratorType):
# DEFAULT, already tokenized, should be generator
return text
elif isinstance(text, tuple):
# OLD: (token, tokens) or a single token
if len(text) == 2:
# (token, tokens)
# chain([token], tokens)
raise NotImplementedError()
else:
# single token
return iter([text])
elif isinstance(text, list):
# OLD: generator from list
return iter(text)
else:
# ?
return text
def _SorTokens(self, tokens, until=',/'):
"""New tokens generator which has S tokens removed,
if followed by anything in ``until``, normally a ``,``."""
for token in tokens:
if token[0] == self.types.S:
try:
next_ = next(tokens)
except StopIteration:
yield token
else:
if next_[1] in until:
# omit S as e.g. ``,`` has been found
yield next_
elif next_[0] == self.types.COMMENT:
# pass COMMENT
yield next_
else:
yield token
yield next_
elif token[0] == self.types.COMMENT:
# pass COMMENT
yield token
else:
yield token
break
# normal mode again
for token in tokens:
yield token
def parse( # noqa: C901
self,
text,
name,
productions,
keepS=False,
checkS=False,
store=None,
emptyOk=False,
debug=False,
):
"""
text (or token generator)
to parse, will be tokenized if not a generator yet
may be:
- a string to be tokenized
- a single token, a tuple
- a tuple of (token, tokensGenerator)
- already tokenized so a tokens generator
name
used for logging
productions
used to parse tokens
keepS
if WS should be added to Seq or just be ignored
store UPDATED
If a Prod defines ``toStore`` the key defined there
is a key in store to be set or if store[key] is a list
the next Item is appended here.
TODO: NEEDED? :
Key ``raw`` is always added and holds all unprocessed
values found
emptyOk
if True text may be empty, hard to test before as may be generator
returns
:wellformed: True or False
:seq: a filled cssutils.util.Seq object which is NOT readonly yet
:store: filled keys defined by Prod.toStore
:unusedtokens: token generator containing tokens not used yet
"""
tokens = self._texttotokens(text)
if not tokens:
self._log.error('No content to parse.')
return False, [], None, None
seq = cssutils.util.Seq(readonly=False)
if not store: # store for specific values
store = {}
prods = [productions] # stack of productions
wellformed = True
# while no real token is found any S are ignored
started = False
stopall = False
prod = None
# flag if default S handling should be done
defaultS = True
stopIfNoMoreMatch = False
while True:
# get from savedTokens or normal tokens
try:
# print debug, "SAVED", savedTokens
token = savedTokens.pop()
except IndexError:
try:
token = next(tokens)
except StopIteration:
break
# print debug, token, stopIfNoMoreMatch
type_, val, line, col = token
# default productions
if type_ == self.types.COMMENT:
# always append COMMENT
seq.append(
cssutils.css.CSSComment(val), cssutils.css.CSSComment, line, col
)
elif defaultS and type_ == self.types.S and not checkS:
# append S (but ignore starting ones)
if not keepS or not started:
continue
else:
seq.append(val, type_, line, col)
# elif type_ == self.types.ATKEYWORD:
# # @rule
# r = cssutils.css.CSSUnknownRule(cssText=val)
# seq.append(r, type(r), line, col)
elif type_ == self.types.INVALID:
# invalidate parse
wellformed = False
self._log.error(f'Invalid token: {token!r}')
break
elif type_ == 'EOF':
# do nothing? (self.types.EOF == True!)
stopall = True
else:
started = True # check S now
try:
while True:
# find next matching production
try:
prod = prods[-1].nextProd(token)
except (Exhausted, NoMatch):
# try next
prod = None
if isinstance(prod, Prod):
# found actual Prod, not a Choice or Sequence
break
elif prod:
# nested Sequence, Choice
prods.append(prod)
else:
# nested exhausted, try in parent
if len(prods) > 1:
prods.pop()
else:
raise NoMatch('No match')
except NoMatch as e:
if stopIfNoMoreMatch: # and token:
# print "\t1stopIfNoMoreMatch", e, token, prod, 'PUSHING'
# tokenizer.push(token)
savedTokens.append(token)
stopall = True
else:
wellformed = False
self._log.error(f'{name}: {e}: {token!r}')
break
except ParseError as e:
# needed???
if stopIfNoMoreMatch: # and token:
# print "\t2stopIfNoMoreMatch", e, token, prod
tokenizer.push(token)
stopall = True
else:
wellformed = False
self._log.error(f'{name}: {e}: {token!r}')
break
else:
# print '\t1', debug, 'PROD', prod
# may stop next time, once set stays
stopIfNoMoreMatch = prod.stopIfNoMoreMatch or stopIfNoMoreMatch
# process prod
if prod.toSeq and not prod.stopAndKeep:
type_, val = prod.toSeq(token, tokens)
if val is not None:
seq.append(val, type_, line, col)
if prod.toStore:
if not prod.storeToken:
prod.toStore(store, seq[-1])
else:
# workaround for now for old style token
# parsing!
# TODO: remove when all new style
prod.toStore(store, token)
if prod.stop:
# stop here and ignore following tokens
# EOF? or end of e.g. func ")"
break
if prod.stopAndKeep: # e.g. ;
# stop here and ignore following tokens
# but keep this token for next run
# TODO: CHECK!!!!
tokenizer.push(token)
tokens = itertools.chain(token, tokens)
stopall = True
break
if prod.nextSor:
# following is S or other token (e.g. ",")?
# remove S if
tokens = self._SorTokens(tokens, ',/')
defaultS = False
else:
defaultS = True
lastprod = prod
# print debug, 'parse done', token, stopall, '\n'
if not stopall:
# stop immediately
while True:
# all productions exhausted?
try:
prod = prods[-1].nextProd(token=None)
except Done:
# ok
prod = None
except Missing as e:
prod = None
# last was a S operator which may End a Sequence, then ok
if hasattr(lastprod, 'mayEnd') and not lastprod.mayEnd:
wellformed = False
self._log.error(f'{name}: {e}')
except ParseError as e:
prod = None
wellformed = False
self._log.error(f'{name}: {e}')
else:
if prods[-1].optional:
prod = None
elif prod and prod.optional:
# ignore optional
continue
if prod and not prod.optional:
wellformed = False
self._log.error(
f'{name}: Missing token for production {str(prod)!r}'
)
break
elif len(prods) > 1:
# nested exhausted, next in parent
prods.pop()
else:
break
if not emptyOk and not len(seq):
self._log.error('No content to parse.')
return False, [], None, None
# trim S from end
seq.rstrip()
return wellformed, seq, store, tokens
class PreDef:
"""Predefined Prod definition for use in productions definition
for ProdParser instances.
"""
types = cssutils.cssproductions.CSSProductions
reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$')
@staticmethod
def calc(toSeq=None, nextSor=False):
return Prod(
name='calcfunction',
match=lambda t, v: 'calc(' == cssutils.helper.normalize(v),
toSeq=toSeq,
nextSor=nextSor,
)
@staticmethod
def char(
name='char',
char=',',
toSeq=None,
stop=False,
stopAndKeep=False,
mayEnd=False,
stopIfNoMoreMatch=False,
optional=False, # WAS: optional=True,
nextSor=False,
):
"any CHAR"
return Prod(
name=name,
match=lambda t, v: v == char,
toSeq=toSeq,
stop=stop,
stopAndKeep=stopAndKeep,
mayEnd=mayEnd,
stopIfNoMoreMatch=stopIfNoMoreMatch,
optional=optional,
nextSor=nextSor,
)
@staticmethod
def comma(optional=False, toSeq=None):
return PreDef.char('comma', ',', optional=optional, toSeq=toSeq)
@staticmethod
def comment(parent=None):
return Prod(
name='comment',
match=lambda t, v: t == 'COMMENT',
toSeq=lambda t, tokens: (
t[0],
cssutils.css.CSSComment([1], parentRule=parent),
),
optional=True,
)
@staticmethod
def dimension(nextSor=False, stop=False):
return Prod(
name='dimension',
match=lambda t, v: t == PreDef.types.DIMENSION,
toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1])),
stop=stop,
nextSor=nextSor,
)
@staticmethod
def function(toSeq=None, nextSor=False, toStore=None):
return Prod(
name='function',
match=lambda t, v: t == PreDef.types.FUNCTION,
toStore=toStore,
toSeq=toSeq,
nextSor=nextSor,
)
@staticmethod
def funcEnd(stop=False, mayEnd=False):
")"
return PreDef.char('end FUNC ")"', ')', stop=stop, mayEnd=mayEnd)
@staticmethod
def hexcolor(stop=False, nextSor=False):
"#123 or #123456"
return Prod(
name='HEX color',
match=lambda t, v: (t == PreDef.types.HASH and PreDef.reHexcolor.match(v)),
stop=stop,
nextSor=nextSor,
)
@staticmethod
def ident(stop=False, toStore=None, nextSor=False):
return Prod(
name='ident',
match=lambda t, v: t == PreDef.types.IDENT,
stop=stop,
toStore=toStore,
nextSor=nextSor,
)
@staticmethod
def number(stop=False, toSeq=None, nextSor=False):
return Prod(
name='number',
match=lambda t, v: t == PreDef.types.NUMBER,
stop=stop,
toSeq=toSeq,
nextSor=nextSor,
)
@staticmethod
def percentage(stop=False, toSeq=None, nextSor=False):
return Prod(
name='percentage',
match=lambda t, v: t == PreDef.types.PERCENTAGE,
stop=stop,
toSeq=toSeq,
nextSor=nextSor,
)
@staticmethod
def string(stop=False, nextSor=False):
"string delimiters are removed by default"
return Prod(
name='string',
match=lambda t, v: t == PreDef.types.STRING,
toSeq=lambda t, tokens: (t[0], cssutils.helper.stringvalue(t[1])),
stop=stop,
nextSor=nextSor,
)
@staticmethod
def S(name='whitespace', toSeq=None, optional=False):
return Prod(
name=name,
match=lambda t, v: t == PreDef.types.S,
toSeq=toSeq,
optional=optional,
mayEnd=True,
)
@staticmethod
def unary(stop=False, toSeq=None, nextSor=False):
"+ or -"
return Prod(
name='unary +-',
match=lambda t, v: v in ('+', '-'),
optional=True,
stop=stop,
toSeq=toSeq,
nextSor=nextSor,
)
@staticmethod
def uri(stop=False, nextSor=False):
"'url(' and ')' are removed and URI is stripped"
return Prod(
name='URI',
match=lambda t, v: t == PreDef.types.URI,
toSeq=lambda t, tokens: (t[0], cssutils.helper.urivalue(t[1])),
stop=stop,
nextSor=nextSor,
)
@staticmethod
def unicode_range(stop=False, nextSor=False):
"u+123456-abc normalized to lower `u`"
return Prod(
name='unicode-range',
match=lambda t, v: t == PreDef.types.UNICODE_RANGE,
toSeq=lambda t, tokens: (t[0], t[1].lower()),
stop=stop,
nextSor=nextSor,
)
@staticmethod
def variable(toSeq=None, stop=False, nextSor=False, toStore=None):
return Prod(
name='variable',
match=lambda t, v: 'var(' == cssutils.helper.normalize(v),
toSeq=toSeq,
toStore=toStore,
stop=stop,
nextSor=nextSor,
)
# used for MarginRule for now:
@staticmethod
def unknownrule(name='@', toStore=None):
"""@rule dummy (matches ATKEYWORD to remove unknown rule tokens from
stream::
@x;
@x {...}
no nested yet!
"""
def rule(tokens):
saved = []
for t in tokens:
saved.append(t)
if t[1] == '}' or t[1] == ';':
return cssutils.css.CSSUnknownRule(saved)
return Prod(
name=name,
match=lambda t, v: t == 'ATKEYWORD',
toSeq=lambda t, tokens: ('CSSUnknownRule', rule(pushtoken(t, tokens))),
toStore=toStore,
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/profiles.py 0000644 0001751 0000177 00000103662 14627633762 017150 0 ustar 00runner docker """CSS profiles.
Profiles is based on code by Kevin D. Smith, orginally used as cssvalues,
thanks!
"""
# too many long lines
# flake8: noqa
__all__ = ['Profiles']
from cssutils import util
import re
class NoSuchProfileException(Exception):
"""Raised if no profile with given name is found"""
pass
class Profiles:
"""
All profiles used for validation. ``cssutils.profile`` is a
preset object of this class and used by all properties for validation.
Predefined profiles are (use
:meth:`~cssutils.profiles.Profiles.propertiesByProfile` to
get a list of defined properties):
``~cssutils.profiles.Profiles.CSS_LEVEL_2``
Properties defined by CSS2.1
``~cssutils.profiles.Profiles.CSS3_BASIC_USER_INTERFACE``
Currently resize and outline properties only
``~cssutils.profiles.Profiles.CSS3_BOX``
Currently overflow related properties only
``~cssutils.profiles.Profiles.CSS3_COLOR``
CSS 3 color properties
``~cssutils.profiles.Profiles.CSS3_PAGED_MEDIA``
As defined at http://www.w3.org/TR/css3-page/ (at 090307)
Predefined macros are:
``~cssutils.profiles.Profiles._TOKEN_MACROS``
Macros containing the token values as defined to CSS2
``~cssutils.profiles.Profiles._MACROS``
Additional general macros.
If you want to redefine any of these macros do this in your custom
macros.
"""
CSS_LEVEL_2 = 'CSS Level 2.1'
CSS3_BACKGROUNDS_AND_BORDERS = 'CSS Backgrounds and Borders Module Level 3'
CSS3_BASIC_USER_INTERFACE = 'CSS3 Basic User Interface Module'
CSS3_BOX = CSS_BOX_LEVEL_3 = 'CSS Box Module Level 3'
CSS3_COLOR = CSS_COLOR_LEVEL_3 = 'CSS Color Module Level 3'
CSS3_FONTS = 'CSS Fonts Module Level 3'
CSS3_FONT_FACE = 'CSS Fonts Module Level 3 @font-face properties'
CSS3_PAGED_MEDIA = 'CSS3 Paged Media Module'
CSS3_TEXT = 'CSS Text Level 3'
_TOKEN_MACROS = {
'ident': r'[-]?{nmstart}{nmchar}*',
'name': r'{nmchar}+',
'nmstart': r'[_a-z]|{nonascii}|{escape}',
'nonascii': r'[^\0-\177]',
'unicode': r'\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?',
'escape': r'{unicode}|\\[ -~\u0080-\u01ff]',
# 'escape': r'{unicode}|\\[ -~\200-\4177777]',
'int': r'[-]?\d+',
'nmchar': r'[\w-]|{nonascii}|{escape}',
'num': r'[-]?\d+|[-]?\d*\.\d+',
'positivenum': r'\d+|\d*\.\d+',
'number': r'{num}',
'string': r'{string1}|{string2}',
'string1': r'"(\\\"|[^\"])*"',
'uri': r'url\({w}({string}|(\\\)|[^\)])+){w}\)',
'string2': r"'(\\\'|[^\'])*'",
'nl': r'\n|\r\n|\r|\f',
'w': r'\s*',
}
_MACROS = {
'hexcolor': r'#[0-9a-f]{3}|#[0-9a-f]{6}',
'rgbcolor': r'rgb\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\)|rgb\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\)',
'namedcolor': r'(transparent|orange|maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray)',
'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)',
'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{uicolor}',
# 'color': r'(maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray|ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)|#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\({w}{int}{w},{w}{int}{w},{w}{int}{w}\)|rgb\({w}{num}%{w},{w}{num}%{w},{w}{num}%{w}\)',
'integer': r'{int}',
'length': r'0|{num}(em|ex|px|in|cm|mm|pt|pc)',
'positivelength': r'0|{positivenum}(em|ex|px|in|cm|mm|pt|pc)',
'angle': r'0|{num}(deg|grad|rad)',
'time': r'0|{num}m?s',
'frequency': r'0|{num}k?Hz',
'percentage': r'{num}%',
'shadow': '(inset)?{w}{length}{w}{length}{w}{length}?{w}{length}?{w}{color}?',
}
def __init__(self, log=None):
"""A few profiles are predefined."""
self._log = log
# macro cache
self._usedMacros = Profiles._TOKEN_MACROS.copy()
self._usedMacros.update(Profiles._MACROS.copy())
# to keep order, REFACTOR!
self._profileNames = []
# for reset if macro changes
self._rawProfiles = {}
# already compiled profiles: {profile: {property: checkfunc, ...}, ...}
self._profilesProperties = {}
self._defaultProfiles = None
self.addProfiles([
(
self.CSS_LEVEL_2,
properties[self.CSS_LEVEL_2],
macros[self.CSS_LEVEL_2],
),
(
self.CSS3_BACKGROUNDS_AND_BORDERS,
properties[self.CSS3_BACKGROUNDS_AND_BORDERS],
macros[self.CSS3_BACKGROUNDS_AND_BORDERS],
),
(
self.CSS3_BASIC_USER_INTERFACE,
properties[self.CSS3_BASIC_USER_INTERFACE],
macros[self.CSS3_BASIC_USER_INTERFACE],
),
(self.CSS3_BOX, properties[self.CSS3_BOX], macros[self.CSS3_BOX]),
(self.CSS3_COLOR, properties[self.CSS3_COLOR], macros[self.CSS3_COLOR]),
(self.CSS3_FONTS, properties[self.CSS3_FONTS], macros[self.CSS3_FONTS]),
# new object for font-face only?
(
self.CSS3_FONT_FACE,
properties[self.CSS3_FONT_FACE],
macros[self.CSS3_FONTS],
),
(
self.CSS3_PAGED_MEDIA,
properties[self.CSS3_PAGED_MEDIA],
macros[self.CSS3_PAGED_MEDIA],
),
(self.CSS3_TEXT, properties[self.CSS3_TEXT], macros[self.CSS3_TEXT]),
])
self.__update_knownNames()
def _expand_macros(self, dictionary, macros):
"""Expand macros in token dictionary"""
def macro_value(m):
return '(?:%s)' % macros[m.groupdict()['macro']]
for key, value in list(dictionary.items()):
if not hasattr(value, '__call__'):
while re.search(r'{[a-z][a-z0-9-]*}', value):
value = re.sub(r'{(?P[a-z][a-z0-9-]*)}', macro_value, value)
dictionary[key] = value
return dictionary
def _compile_regexes(self, dictionary):
"""Compile all regular expressions into callable objects"""
for key, value in list(dictionary.items()):
if not hasattr(value, '__call__'):
# Compiling them now will slow down the cssutils import time,
# even if cssutils is not needed. Thus, lazily compile them the
# first time they're needed.
# https://web.archive.org/web/20200701035537/https://bitbucket.org/cthedot/cssutils/issues/69)
value = util.LazyRegex('^(?:%s)$' % value, re.I)
dictionary[key] = value
return dictionary
def __update_knownNames(self):
self._knownNames = []
for properties in list(self._profilesProperties.values()):
self._knownNames.extend(list(properties.keys()))
def _getDefaultProfiles(self):
"If not explicitly set same as Profiles.profiles but in reverse order."
if not self._defaultProfiles:
return self.profiles
else:
return self._defaultProfiles
def _setDefaultProfiles(self, profiles):
"profiles may be a single or a list of profile names"
if isinstance(profiles, str):
self._defaultProfiles = (profiles,)
else:
self._defaultProfiles = profiles
defaultProfiles = property(
_getDefaultProfiles,
_setDefaultProfiles,
doc="Names of profiles to use for validation."
"To use e.g. the CSS2 profile set "
"``cssutils.profile.defaultProfiles = "
"cssutils.profile.CSS_LEVEL_2``",
)
profiles = property(
lambda self: self._profileNames,
doc='Names of all profiles in order as defined.',
)
@property
def knownNames(self):
"""All known property names of all profiles."""
return self._knownNames
def _resetProperties(self, newMacros=None):
"reset all props from raw values as changes in macros happened"
# base
macros = Profiles._TOKEN_MACROS.copy()
macros.update(Profiles._MACROS.copy())
# former
for profile in self._profileNames:
macros.update(self._rawProfiles[profile]['macros'])
# new
if newMacros:
macros.update(newMacros)
# reset properties
self._profilesProperties.clear()
for profile in self._profileNames:
properties = self._expand_macros(
# keep raw
self._rawProfiles[profile]['properties'].copy(),
macros,
)
self._profilesProperties[profile] = self._compile_regexes(properties)
# save
self._usedMacros = macros
def addProfiles(self, profiles):
"""Add a list of profiles at once. Useful as if profiles define custom
macros these are used in one go. Using `addProfile` instead my be
**very** slow instead.
"""
# add macros
for profile, properties, macros in profiles:
if macros:
self._usedMacros.update(macros)
self._rawProfiles[profile] = {'macros': macros.copy()}
# only add new properties
for profile, properties, macros in profiles:
self.addProfile(profile, properties.copy(), None)
def addProfile(self, profile, properties, macros=None):
"""Add a new profile with name `profile` (e.g. 'CSS level 2')
and the given `properties`.
:param profile:
the new `profile`'s name
:param properties:
a dictionary of ``{ property-name: propery-value }`` items where
property-value is a regex which may use macros defined in given
``macros`` or the standard macros Profiles.tokens and
Profiles.generalvalues.
``propery-value`` may also be a function which takes a single
argument which is the value to validate and which should return
True or False.
Any exceptions which may be raised during this custom validation
are reported or raised as all other cssutils exceptions depending
on cssutils.log.raiseExceptions which e.g during parsing normally
is False so the exceptions would be logged only.
:param macros:
may be used in the given properties definitions. There are some
predefined basic macros which may always be used in
``Profiles._TOKEN_MACROS`` and ``Profiles._MACROS``.
"""
if macros:
# check if known macros would change and if yes reset properties
if len(set(macros.keys()).intersection(list(self._usedMacros.keys()))):
self._resetProperties(newMacros=macros)
else:
# no replacement, simply continue
self._usedMacros.update(macros)
else:
# might have been set by addProfiles before
try:
macros = self._rawProfiles[profile]['macros']
except KeyError:
macros = {}
# save name and raw props/macros if macros change to completely reset
self._profileNames.append(profile)
self._rawProfiles[profile] = {
'properties': properties.copy(),
'macros': macros.copy(),
}
# prepare and save properties
properties = self._expand_macros(properties, self._usedMacros)
self._profilesProperties[profile] = self._compile_regexes(properties)
self.__update_knownNames()
def removeProfile(self, profile=None, all=False):
"""Remove `profile` or remove `all` profiles.
If the removed profile used custom macros all remaining profiles
are reset to reflect the macro changes. This may be quite an expensive
operation!
:param profile:
profile name to remove
:param all:
if ``True`` removes all profiles to start with a clean state
:exceptions:
- ``cssutils.profiles.NoSuchProfileException``:
If given `profile` cannot be found.
"""
if all:
self._profilesProperties.clear()
self._rawProfiles.clear()
del self._profileNames[:]
else:
reset = False
try:
if self._rawProfiles[profile]['macros']:
reset = True
del self._profilesProperties[profile]
del self._rawProfiles[profile]
del self._profileNames[self._profileNames.index(profile)]
except KeyError:
raise NoSuchProfileException('No profile %r.' % profile)
else:
if reset:
# reset properties as macros were removed
self._resetProperties()
self.__update_knownNames()
def propertiesByProfile(self, profiles=None):
"""Generator: Yield property names, if no `profiles` is given all
profile's properties are used.
:param profiles:
a single profile name or a list of names.
"""
if not profiles:
profiles = self.profiles
elif isinstance(profiles, str):
profiles = (profiles,)
try:
for profile in sorted(profiles):
yield from sorted(self._profilesProperties[profile].keys())
except KeyError as e:
raise NoSuchProfileException(e)
def validate(self, name, value):
"""Check if `value` is valid for given property `name` using **any**
profile.
:param name:
a property name
:param value:
a CSS value (string)
:returns:
if the `value` is valid for the given property `name` in any
profile
"""
for profile in self.profiles:
if name in self._profilesProperties[profile]:
try:
# custom validation errors are caught
r = bool(self._profilesProperties[profile][name](value))
except Exception as e:
# TODO: more specific exception?
# Validate should not be fatal though!
self._log.error(e, error=Exception)
r = False
if r:
return r
return False
def validateWithProfile(self, name, value, profiles=None): # noqa: C901
"""Check if `value` is valid for given property `name` returning
``(valid, profile)``.
:param name:
a property name
:param value:
a CSS value (string)
:param profiles:
internal parameter used by Property.validate only
:returns:
``valid, matching, profiles`` where ``valid`` is if the `value`
is valid for the given property `name` in any profile,
``matching==True`` if it is valid in the given `profiles`
and ``profiles`` the profile names for which the value is valid
(or ``[]`` if not valid at all)
Example::
> cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2
> print(cssutils.profile.validateWithProfile('color', 'rgba(1,1,1,1)'))
(True, False, Profiles.CSS3_COLOR)
"""
if name not in self.knownNames:
return False, False, []
else:
if not profiles:
profiles = self.defaultProfiles
elif isinstance(profiles, str):
profiles = (profiles,)
for profilename in reversed(profiles):
# check given profiles
if name in self._profilesProperties[profilename]:
validate = self._profilesProperties[profilename][name]
try:
if validate(value):
return True, True, [profilename]
except Exception as e:
self._log.error(e, error=Exception)
for profilename in (p for p in self._profileNames if p not in profiles):
# check remaining profiles as well
if name in self._profilesProperties[profilename]:
validate = self._profilesProperties[profilename][name]
try:
if validate(value):
return True, False, [profilename]
except Exception as e:
self._log.error(e, error=Exception)
names = []
for profilename, properties in list(self._profilesProperties.items()):
# return profile to which name belongs
if name in list(properties.keys()):
names.append(profilename)
names.sort()
return False, False, names
properties = {}
macros = {}
"""
Define some regular expression fragments that will be used as
macros within the CSS property value regular expressions.
"""
macros[Profiles.CSS_LEVEL_2] = {
'background-color': r'{color}|transparent|inherit',
'background-image': r'{uri}|none|inherit',
# 'background-position': r'({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit',
'background-position': r'({percentage}|{length}|left|center|right)(\s*({percentage}|{length}|top|center|bottom))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit',
'background-repeat': r'repeat|repeat-x|repeat-y|no-repeat|inherit',
'background-attachment': r'scroll|fixed|inherit',
'shape': r'rect\(({w}({length}|auto}){w},){3}{w}({length}|auto){w}\)',
'counter': r'counter\({w}{ident}{w}(?:,{w}{list-style-type}{w})?\)',
'identifier': r'{ident}',
'family-name': r'{string}|({ident}(\s+{ident})*)',
'generic-family': r'serif|sans-serif|cursive|fantasy|monospace',
'absolute-size': r'(x?x-)?(small|large)|medium',
'relative-size': r'smaller|larger',
'font-family': r'({family-name}({w},{w}{family-name})*)|inherit',
'font-size': r'{absolute-size}|{relative-size}|{positivelength}|{percentage}|inherit',
'font-style': r'normal|italic|oblique|inherit',
'font-variant': r'normal|small-caps|inherit',
'font-weight': r'normal|bold|bolder|lighter|[1-9]00|inherit',
'line-height': r'normal|{number}|{length}|{percentage}|inherit',
'list-style-image': r'{uri}|none|inherit',
'list-style-position': r'inside|outside|inherit',
'list-style-type': r'disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-(latin|alpha)|upper-(latin|alpha)|armenian|georgian|none|inherit',
'margin-width': r'{length}|{percentage}|auto',
'padding-width': r'{length}|{percentage}',
'specific-voice': r'{ident}',
'generic-voice': r'male|female|child',
'content': r'{string}|{uri}|{counter}|attr\({w}{ident}{w}\)|open-quote|close-quote|no-open-quote|no-close-quote',
'background-attrs': r'{background-color}|{background-image}|{background-repeat}|{background-attachment}|{background-position}',
'list-attrs': r'{list-style-type}|{list-style-position}|{list-style-image}',
'font-attrs': r'{font-style}|{font-variant}|{font-weight}',
'text-attrs': r'underline|overline|line-through|blink',
'overflow': r'visible|hidden|scroll|auto|inherit',
}
"""
Define the regular expressions for validation all CSS values
"""
properties[Profiles.CSS_LEVEL_2] = {
'azimuth': r'{angle}|(behind\s+)?(left-side|far-left|left|center-left|center|center-right|right|far-right|right-side)(\s+behind)?|behind|leftwards|rightwards|inherit',
'background-attachment': r'{background-attachment}',
'background-color': r'{background-color}',
'background-image': r'{background-image}',
'background-position': r'{background-position}',
'background-repeat': r'{background-repeat}',
# Each piece should only be allowed one time
'background': r'{background-attrs}(\s+{background-attrs})*|inherit',
'border-collapse': r'collapse|separate|inherit',
'border-spacing': r'{length}(\s+{length})?|inherit',
'bottom': r'{length}|{percentage}|auto|inherit',
'caption-side': r'top|bottom|inherit',
'clear': r'none|left|right|both|inherit',
'clip': r'{shape}|auto|inherit',
'color': r'{color}|inherit',
'content': r'none|normal|{content}(\s+{content})*|inherit',
'counter-increment': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit',
'counter-reset': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit',
'cue-after': r'{uri}|none|inherit',
'cue-before': r'{uri}|none|inherit',
'cue': r'({uri}|none|inherit){1,2}|inherit',
# 'cursor': r'((({uri}{w},{w})*)?(auto|crosshair|default|pointer|move|(e|ne|nw|n|se|sw|s|w)-resize|text|wait|help|progress))|inherit',
'direction': r'ltr|rtl|inherit',
'display': r'inline|block|list-item|run-in|inline-block|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|none|inherit',
'elevation': r'{angle}|below|level|above|higher|lower|inherit',
'empty-cells': r'show|hide|inherit',
'float': r'left|right|none|inherit',
'font-family': r'{font-family}',
'font-size': r'{font-size}',
'font-style': r'{font-style}',
'font-variant': r'{font-variant}',
'font-weight': r'{font-weight}',
'font': r'(({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{font-family})|caption|icon|menu|message-box|small-caption|status-bar|inherit',
'height': r'{length}|{percentage}|auto|inherit',
'left': r'{length}|{percentage}|auto|inherit',
'letter-spacing': r'normal|{length}|inherit',
'line-height': r'{line-height}',
'list-style-image': r'{list-style-image}',
'list-style-position': r'{list-style-position}',
'list-style-type': r'{list-style-type}',
'list-style': r'{list-attrs}(\s+{list-attrs})*|inherit',
'margin-right': r'{margin-width}|inherit',
'margin-left': r'{margin-width}|inherit',
'margin-top': r'{margin-width}|inherit',
'margin-bottom': r'{margin-width}|inherit',
'margin': r'{margin-width}(\s+{margin-width}){0,3}|inherit',
'max-height': r'{length}|{percentage}|none|inherit',
'max-width': r'{length}|{percentage}|none|inherit',
'min-height': r'{length}|{percentage}|none|inherit',
'min-width': r'{length}|{percentage}|none|inherit',
'orphans': r'{integer}|inherit',
'overflow': r'{overflow}',
'padding-top': r'{padding-width}|inherit',
'padding-right': r'{padding-width}|inherit',
'padding-bottom': r'{padding-width}|inherit',
'padding-left': r'{padding-width}|inherit',
'padding': r'{padding-width}(\s+{padding-width}){0,3}|inherit',
'page-break-after': r'auto|always|avoid|left|right|inherit',
'page-break-before': r'auto|always|avoid|left|right|inherit',
'page-break-inside': r'avoid|auto|inherit',
'pause-after': r'{time}|{percentage}|inherit',
'pause-before': r'{time}|{percentage}|inherit',
'pause': r'({time}|{percentage}){1,2}|inherit',
'pitch-range': r'{number}|inherit',
'pitch': r'{frequency}|x-low|low|medium|high|x-high|inherit',
'play-during': r'{uri}(\s+(mix|repeat))*|auto|none|inherit',
'position': r'static|relative|absolute|fixed|inherit',
'quotes': r'({string}\s+{string})(\s+{string}\s+{string})*|none|inherit',
'richness': r'{number}|inherit',
'right': r'{length}|{percentage}|auto|inherit',
'speak-header': r'once|always|inherit',
'speak-numeral': r'digits|continuous|inherit',
'speak-punctuation': r'code|none|inherit',
'speak': r'normal|none|spell-out|inherit',
'speech-rate': r'{number}|x-slow|slow|medium|fast|x-fast|faster|slower|inherit',
'stress': r'{number}|inherit',
'table-layout': r'auto|fixed|inherit',
'text-align': r'left|right|center|justify|inherit',
'text-decoration': r'none|{text-attrs}(\s+{text-attrs})*|inherit',
'text-indent': r'{length}|{percentage}|inherit',
'text-transform': r'capitalize|uppercase|lowercase|none|inherit',
'top': r'{length}|{percentage}|auto|inherit',
'unicode-bidi': r'normal|embed|bidi-override|inherit',
'vertical-align': r'baseline|sub|super|top|text-top|middle|bottom|text-bottom|{percentage}|{length}|inherit',
'visibility': r'visible|hidden|collapse|inherit',
'voice-family': r'({specific-voice}|{generic-voice}{w},{w})*({specific-voice}|{generic-voice})|inherit',
'volume': r'{number}|{percentage}|silent|x-soft|soft|medium|loud|x-loud|inherit',
'white-space': r'normal|pre|nowrap|pre-wrap|pre-line|inherit',
'widows': r'{integer}|inherit',
'width': r'{length}|{percentage}|auto|inherit',
'word-spacing': r'normal|{length}|inherit',
'z-index': r'auto|{integer}|inherit',
}
macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = {
'border-style': 'none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset',
'border-width': '{length}|thin|medium|thick',
'b1': r'{border-width}?({w}{border-style})?({w}{color})?',
'b2': r'{border-width}?({w}{color})?({w}{border-style})?',
'b3': r'{border-style}?({w}{border-width})?({w}{color})?',
'b4': r'{border-style}?({w}{color})?({w}{border-width})?',
'b5': r'{color}?({w}{border-style})?({w}{border-width})?',
'b6': r'{color}?({w}{border-width})?({w}{border-style})?',
'border-attrs': r'{b1}|{b2}|{b3}|{b4}|{b5}|{b6}',
'border-radius-part': r'({length}|{percentage})(\s+({length}|{percentage}))?',
}
properties[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = {
'border-color': r'({color}|transparent)(\s+({color}|transparent)){0,3}|inherit',
'border-style': r'{border-style}(\s+{border-style}){0,3}|inherit',
'border-top': r'{border-attrs}|inherit',
'border-right': r'{border-attrs}|inherit',
'border-bottom': r'{border-attrs}|inherit',
'border-left': r'{border-attrs}|inherit',
'border-top-color': r'{color}|transparent|inherit',
'border-right-color': r'{color}|transparent|inherit',
'border-bottom-color': r'{color}|transparent|inherit',
'border-left-color': r'{color}|transparent|inherit',
'border-top-style': r'{border-style}|inherit',
'border-right-style': r'{border-style}|inherit',
'border-bottom-style': r'{border-style}|inherit',
'border-left-style': r'{border-style}|inherit',
'border-top-width': r'{border-width}|inherit',
'border-right-width': r'{border-width}|inherit',
'border-bottom-width': r'{border-width}|inherit',
'border-left-width': r'{border-width}|inherit',
'border-width': r'{border-width}(\s+{border-width}){0,3}|inherit',
'border': r'{border-attrs}|inherit',
'border-top-right-radius': '{border-radius-part}',
'border-bottom-right-radius': '{border-radius-part}',
'border-bottom-left-radius': '{border-radius-part}',
'border-top-left-radius': '{border-radius-part}',
'border-radius': '({length}{w}|{percentage}{w}){1,4}(/{w}({length}{w}|{percentage}{w}){1,4})?',
'box-shadow': 'none|{shadow}({w},{w}{shadow})*',
}
# CSS3 Basic User Interface Module
macros[Profiles.CSS3_BASIC_USER_INTERFACE] = {
'border-style': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-style'],
'border-width': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-width'],
'outline-1': r'{outline-color}(\s+{outline-style})?(\s+{outline-width})?',
'outline-2': r'{outline-color}(\s+{outline-width})?(\s+{outline-style})?',
'outline-3': r'{outline-style}(\s+{outline-color})?(\s+{outline-width})?',
'outline-4': r'{outline-style}(\s+{outline-width})?(\s+{outline-color})?',
'outline-5': r'{outline-width}(\s+{outline-color})?(\s+{outline-style})?',
'outline-6': r'{outline-width}(\s+{outline-style})?(\s+{outline-color})?',
'outline-color': r'{color}|invert|inherit',
'outline-style': r'auto|{border-style}|inherit',
'outline-width': r'{border-width}|inherit',
}
properties[Profiles.CSS3_BASIC_USER_INTERFACE] = {
'box-sizing': r'content-box|border-box',
'cursor': r'((({uri}{w}({number}{w}{number}{w})?,{w})*)?(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|(e|n|ne|nw|s|se|sw|w|ew|ns|nesw|nwse|col|row)-resize|all-scroll))|inherit',
'nav-index': r'auto|{number}|inherit',
'outline-color': r'{outline-color}',
'outline-style': r'{outline-style}',
'outline-width': r'{outline-width}',
'outline-offset': r'{length}|inherit',
# 'outline': r'{outline-attrs}(\s+{outline-attrs})*|inherit',
'outline': r'{outline-1}|{outline-2}|{outline-3}|{outline-4}|{outline-5}|{outline-6}|inherit',
'resize': 'none|both|horizontal|vertical|inherit',
}
# CSS Box Module Level 3
macros[Profiles.CSS3_BOX] = {'overflow': macros[Profiles.CSS_LEVEL_2]['overflow']}
properties[Profiles.CSS3_BOX] = {
'overflow': '{overflow}{w}{overflow}?|inherit',
'overflow-x': '{overflow}|inherit',
'overflow-y': '{overflow}|inherit',
}
# CSS Color Module Level 3
macros[Profiles.CSS3_COLOR] = {
# orange and transparent in CSS 2.1
'namedcolor': r'(currentcolor|transparent|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)',
# orange?
'rgbacolor': r'rgba\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\,{w}{num}{w}\)|rgba\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)',
'hslcolor': r'hsl\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\)|hsla\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)',
'x11color': r'aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen',
'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)',
'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{rgbacolor}|{hslcolor}|{x11color}|inherit',
}
properties[Profiles.CSS3_COLOR] = {
'opacity': r'{num}|inherit',
}
# CSS Fonts Module Level 3 http://www.w3.org/TR/css3-fonts/
macros[Profiles.CSS3_FONTS] = {
# 'family-name': r'{string}|{ident}',
'family-name': r'{string}|({ident}(\s+{ident})*)',
'font-face-name': r'local\({w}{family-name}{w}\)',
'font-stretch-names': r'(ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)',
'unicode-range': r'[uU]\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?',
}
properties[Profiles.CSS3_FONTS] = {
'font-size-adjust': r'{number}|none|inherit',
'font-stretch': r'normal|wider|narrower|{font-stretch-names}|inherit',
}
properties[Profiles.CSS3_FONT_FACE] = {
'font-family': '{family-name}',
'font-stretch': r'{font-stretch-names}',
'font-style': r'normal|italic|oblique',
'font-weight': r'normal|bold|[1-9]00',
'src': r'({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name})({w},{w}({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name}))*',
'unicode-range': '{unicode-range}({w},{w}{unicode-range})*',
}
# CSS3 Paged Media
macros[Profiles.CSS3_PAGED_MEDIA] = {
'page-size': 'a5|a4|a3|b5|b4|letter|legal|ledger',
'page-orientation': 'portrait|landscape',
'page-1': '{page-size}(?:{w}{page-orientation})?',
'page-2': '{page-orientation}(?:{w}{page-size})?',
'page-size-orientation': '{page-1}|{page-2}',
'pagebreak': 'auto|always|avoid|left|right',
}
properties[Profiles.CSS3_PAGED_MEDIA] = {
'fit': 'fill|hidden|meet|slice',
'fit-position': r'auto|(({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?))',
'image-orientation': 'auto|{angle}',
'orphans': r'{integer}|inherit',
'page': 'auto|{ident}',
'page-break-before': '{pagebreak}|inherit',
'page-break-after': '{pagebreak}|inherit',
'page-break-inside': 'auto|avoid|inherit',
'size': '({length}{w}){1,2}|auto|{page-size-orientation}',
'widows': r'{integer}|inherit',
}
macros[Profiles.CSS3_TEXT] = {}
properties[Profiles.CSS3_TEXT] = {
'text-shadow': 'none|{shadow}({w},{w}{shadow})*',
}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/sac.py 0000644 0001751 0000177 00000041337 14627633762 016073 0 ustar 00runner docker """A validating CSSParser"""
import codecs
import sys
from . import errorhandler, helper, tokenize2
class ErrorHandler:
"""Basic class for CSS error handlers.
This class class provides a default implementation ignoring warnings and
recoverable errors and throwing a SAXParseException for fatal errors.
If a CSS application needs to implement customized error handling, it must
extend this class and then register an instance with the CSS parser
using the parser's setErrorHandler method. The parser will then report all
errors and warnings through this interface.
The parser shall use this class instead of throwing an exception: it is
up to the application whether to throw an exception for different types of
errors and warnings. Note, however, that there is no requirement that the
parser continue to provide useful information after a call to fatalError
(in other words, a CSS driver class could catch an exception and report a
fatalError).
"""
def __init__(self):
self._log = errorhandler.ErrorHandler()
def error(self, exception, token=None):
self._log.error(exception, token, neverraise=True)
def fatal(self, exception, token=None):
self._log.fatal(exception, token)
def warn(self, exception, token=None):
self._log.warn(exception, token, neverraise=True)
class DocumentHandler:
"""
void endFontFace()
Receive notification of the end of a font face statement.
void endMedia(SACMediaList media)
Receive notification of the end of a media statement.
void endPage(java.lang.String name, java.lang.String pseudo_page)
Receive notification of the end of a media statement.
void importStyle(java.lang.String uri, SACMediaList media,
java.lang.String defaultNamespaceURI)
Receive notification of a import statement in the style sheet.
void startFontFace()
Receive notification of the beginning of a font face statement.
void startMedia(SACMediaList media)
Receive notification of the beginning of a media statement.
void startPage(java.lang.String name, java.lang.String pseudo_page)
Receive notification of the beginning of a page statement.
"""
def __init__(self):
def log(msg):
sys.stderr.write('INFO\t%s\n' % msg)
self._log = log
def comment(self, text, line=None, col=None):
"Receive notification of a comment."
self._log(f"comment {text!r} at [{line}, {col}]")
def startDocument(self, encoding):
"Receive notification of the beginning of a style sheet."
# source
self._log("startDocument encoding=%s" % encoding)
def endDocument(self, source=None, line=None, col=None):
"Receive notification of the end of a document."
self._log("endDocument EOF")
def importStyle(self, uri, media, name, line=None, col=None):
"Receive notification of a import statement in the style sheet."
# defaultNamespaceURI???
self._log(f"importStyle at [{line}, {col}]")
def namespaceDeclaration(self, prefix, uri, line=None, col=None):
"Receive notification of an unknown rule t-rule not supported by this parser."
# prefix might be None!
self._log(f"namespaceDeclaration at [{line}, {col}]")
def startSelector(self, selectors=None, line=None, col=None):
"Receive notification of the beginning of a rule statement."
# TODO selectorList!
self._log(f"startSelector at [{line}, {col}]")
def endSelector(self, selectors=None, line=None, col=None):
"Receive notification of the end of a rule statement."
self._log(f"endSelector at [{line}, {col}]")
def property(self, name, value='TODO', important=False, line=None, col=None):
"Receive notification of a declaration."
# TODO: value is LexicalValue?
self._log(f"property {name!r} at [{line}, {col}]")
def ignorableAtRule(self, atRule, line=None, col=None):
"Receive notification of an unknown rule t-rule not supported by this parser."
self._log(f"ignorableAtRule {atRule!r} at [{line}, {col}]")
class EchoHandler(DocumentHandler):
"Echos all input to property `out`"
def __init__(self):
super().__init__()
self._out = []
out = property(lambda self: ''.join(self._out))
def startDocument(self, encoding):
super().startDocument(encoding)
if 'utf-8' != encoding:
self._out.append('@charset "%s";\n' % encoding)
def importStyle(self, uri, media, name, line=None, col=None):
"Receive notification of a import statement in the style sheet."
# defaultNamespaceURI???
super().importStyle(uri, media, name, line, col)
self._out.append(
'@import %s%s%s;\n'
% (
helper.string(uri),
'%s ' % media if media else '',
'%s ' % name if name else '',
)
)
def namespaceDeclaration(self, prefix, uri, line=None, col=None):
super().namespaceDeclaration(prefix, uri, line, col)
self._out.append(
'@namespace %s%s;\n'
% ('%s ' % prefix if prefix else '', helper.string(uri))
)
def startSelector(self, selectors=None, line=None, col=None):
super().startSelector(selectors, line, col)
if selectors:
self._out.append(', '.join(selectors))
self._out.append(' {\n')
def endSelector(self, selectors=None, line=None, col=None):
self._out.append(' }')
def property(self, name, value, important=False, line=None, col=None):
super().property(name, value, line, col)
self._out.append(
' {}: {}{};\n'.format(name, value, ' !important' if important else '')
)
class Parser:
"""
java.lang.String getParserVersion()
Returns a string about which CSS language is supported by this parser.
boolean parsePriority(InputSource source)
Parse a CSS priority value (e.g.
LexicalUnit parsePropertyValue(InputSource source)
Parse a CSS property value.
void parseRule(InputSource source)
Parse a CSS rule.
SelectorList parseSelectors(InputSource source)
Parse a comma separated list of selectors.
void parseStyleDeclaration(InputSource source)
Parse a CSS style declaration (without '{' and '}').
void parseStyleSheet(InputSource source)
Parse a CSS document.
void parseStyleSheet(java.lang.String uri)
Parse a CSS document from a URI.
void setConditionFactory(ConditionFactory conditionFactory)
void setDocumentHandler(DocumentHandler handler)
Allow an application to register a document event handler.
void setErrorHandler(ErrorHandler handler)
Allow an application to register an error event handler.
void setLocale(java.util.Locale locale)
Allow an application to request a locale for errors and warnings.
void setSelectorFactory(SelectorFactory selectorFactory)
"""
def __init__(self, documentHandler=None, errorHandler=None):
self._tokenizer = tokenize2.Tokenizer()
if documentHandler:
self.setDocumentHandler(documentHandler)
else:
self.setDocumentHandler(DocumentHandler())
if errorHandler:
self.setErrorHandler(errorHandler)
else:
self.setErrorHandler(ErrorHandler())
def parseString(self, cssText, encoding=None): # noqa: C901
if isinstance(cssText, str):
cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0]
tokens = self._tokenizer.tokenize(cssText, fullsheet=True)
def COMMENT(val, line, col):
self._handler.comment(val[2:-2], line, col)
def EOF(val, line, col):
self._handler.endDocument(val, line, col)
def simple(t):
map = {'COMMENT': COMMENT, 'S': lambda val, line, col: None, 'EOF': EOF}
type_, val, line, col = t
if type_ in map:
map[type_](val, line, col)
return True
else:
return False
# START PARSING
t = next(tokens)
type_, val, line, col = t
encoding = 'utf-8'
if 'CHARSET_SYM' == type_:
# @charset "encoding";
# S
encodingtoken = next(tokens)
semicolontoken = next(tokens)
if 'STRING' == type_:
encoding = helper.stringvalue(val)
# ;
if 'STRING' == encodingtoken[0] and semicolontoken:
encoding = helper.stringvalue(encodingtoken[1])
else:
self._errorHandler.fatal('Invalid @charset')
t = next(tokens)
type_, val, line, col = t
self._handler.startDocument(encoding)
while True:
start = (line, col)
try:
if simple(t):
pass
elif 'ATKEYWORD' == type_ or type_ in (
'PAGE_SYM',
'MEDIA_SYM',
'FONT_FACE_SYM',
):
atRule = [val]
braces = 0
while True:
# read till end ;
# TODO: or {}
t = next(tokens)
type_, val, line, col = t
atRule.append(val)
if ';' == val and not braces:
break
elif '{' == val:
braces += 1
elif '}' == val:
braces -= 1
if braces == 0:
break
self._handler.ignorableAtRule(''.join(atRule), *start)
elif 'IMPORT_SYM' == type_:
# import URI or STRING media? name?
uri, media, name = None, None, None
while True:
t = next(tokens)
type_, val, line, col = t
if 'STRING' == type_:
uri = helper.stringvalue(val)
elif 'URI' == type_:
uri = helper.urivalue(val)
elif ';' == val:
break
if uri:
self._handler.importStyle(uri, media, name)
else:
self._errorHandler.error(
'Invalid @import' ' declaration at %r' % (start,)
)
elif 'NAMESPACE_SYM' == type_:
prefix, uri = None, None
while True:
t = next(tokens)
type_, val, line, col = t
if 'IDENT' == type_:
prefix = val
elif 'STRING' == type_:
uri = helper.stringvalue(val)
elif 'URI' == type_:
uri = helper.urivalue(val)
elif ';' == val:
break
if uri:
self._handler.namespaceDeclaration(prefix, uri, *start)
else:
self._errorHandler.error(
'Invalid @namespace' ' declaration at %r' % (start,)
)
else:
# CSSSTYLERULE
selector = []
selectors = []
while True:
# selectors[, selector]* {
if 'S' == type_:
selector.append(' ')
elif simple(t):
pass
elif ',' == val:
selectors.append(''.join(selector).strip())
selector = []
elif '{' == val:
selectors.append(''.join(selector).strip())
self._handler.startSelector(selectors, *start)
break
else:
selector.append(val)
t = next(tokens)
type_, val, line, col = t
end = None
while True:
# name: value [!important][;name: value [!important]]*;?
name, value, important = None, [], False
while True:
# name:
t = next(tokens)
type_, val, line, col = t
if 'S' == type_:
pass
elif simple(t):
pass
elif 'IDENT' == type_:
if name:
self._errorHandler.error(
'more than one property name', t
)
else:
name = val
elif ':' == val:
if not name:
self._errorHandler.error('no property name', t)
break
elif ';' == val:
self._errorHandler.error('premature end of property', t)
end = val
break
elif '}' == val:
if name:
self._errorHandler.error(
'premature end of property', t
)
end = val
break
else:
self._errorHandler.error(
'unexpected property name token %r' % val, t
)
while not ';' == end and not '}' == end:
# value !;}
t = next(tokens)
type_, val, line, col = t
if 'S' == type_:
value.append(' ')
elif simple(t):
pass
elif '!' == val or ';' == val or '}' == val:
value = ''.join(value).strip()
if not value:
self._errorHandler.error(
'premature end of property (no value)', t
)
end = val
break
else:
value.append(val)
while '!' == end:
# !important
t = next(tokens)
type_, val, line, col = t
if simple(t):
pass
elif 'IDENT' == type_ and not important:
important = True
elif ';' == val or '}' == val:
end = val
break
else:
self._errorHandler.error(
'unexpected priority token %r' % val
)
if name and value:
self._handler.property(name, value, important)
if '}' == end:
self._handler.endSelector(selectors, line=line, col=col)
break
else:
# reset
end = None
else:
self._handler.endSelector(selectors, line=line, col=col)
t = next(tokens)
type_, val, line, col = t
except StopIteration:
break
def setDocumentHandler(self, handler):
"Allow an application to register a document event `handler`."
self._handler = handler
def setErrorHandler(self, handler):
"TODO"
self._errorHandler = handler
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1717516274.0
cssutils-2.11.1/cssutils/script.py 0000644 0001751 0000177 00000027502 14627633762 016627 0 ustar 00runner docker """classes and functions used by cssutils scripts"""
__all__ = ['CSSCapture', 'csscombine']
import codecs
import errno
import html.parser
import logging
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
import cssutils
import encutils
# types of sheets in HTML
LINK = (
0 #
)
STYLE = 1 #
class CSSCaptureHTMLParser(html.parser.HTMLParser):
"""CSSCapture helper: Parse given data for link and style elements"""
curtag = ''
sheets = [] # (type, [atts, cssText])
def _loweratts(self, atts):
return {a.lower(): v.lower() for a, v in atts}
def handle_starttag(self, tag, atts):
if tag == 'link':
atts = self._loweratts(atts)
if 'text/css' == atts.get('type', ''):
self.sheets.append((LINK, atts))
elif tag == 'style':
# also get content of style
atts = self._loweratts(atts)
if 'text/css' == atts.get('type', ''):
self.sheets.append((STYLE, [atts, '']))
self.curtag = tag
else:
# close as only intersting