Source code for dict2css

#!/usr/bin/env python3
#
#  __init__.py
"""
A μ-library for constructing cascasing style sheets from Python dictionaries.
"""
#
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
from io import TextIOBase
from typing import IO, Any, Dict, Iterable, List, Literal, Mapping, MutableMapping, Optional, Sequence, Union

# 3rd party
import tinycss2  # type: ignore[import-untyped]
import tinycss2.ast  # type: ignore[import-untyped]
from domdf_python_tools.paths import PathPlus
from domdf_python_tools.typing import PathLike
from domdf_python_tools.words import TAB

# this package
from dict2css.helpers import em, px, rem  # noqa: F401
from dict2css.serializer import CSSSerializer

__author__: str = "Dominic Davis-Foster"
__copyright__: str = "2020-2026 Dominic Davis-Foster"
__license__: str = "MIT License"
__version__: str = "0.6.0"
__email__: str = "dominic@davis-foster.co.uk"

__all__ = [
		"IMPORTANT",
		"Style",
		"dumps",
		"dump",
		"loads",
		"load",
		]

# TODO: allow int indent like json.dumps etc.

IMPORTANT = "important"
"""
The string ``'important'``.

.. latex:vspace:: 10px
"""

# Property = Union[Tuple[Union[str, int, None], str], str, int, None]
Property = Union[Sequence, str, int, float, None]

Style = Mapping[str, Property]
"""
Type annotation representing a style for :func:`~.dumps` and :func:`~.dump`.

The keys are CSS properties.

The values can be either:

* A :class:`str`, :class:`float` or :py:obj:`None`, giving the value of the property.
* A :class:`tuple` of the property's value (as above) and the priority
  such as :data:`~.IMPORTANT` (which sets ``!important`` on the property).
"""


[docs]def dumps( styles: Mapping[str, Union[Style, Mapping]], *, indent: str = TAB, trailing_semicolon: Optional[bool] = False, indent_closing_brace: bool = False, minify: bool = False, sort_keys: bool = False, check_circular: bool = True, none_style: Union[Literal["none"], Literal["None"]] = "none", ) -> str: r""" Construct a cascading style sheet from a dictionary. ``styles`` is a mapping of CSS selector strings to styles, which map property names to their values: .. code-block:: python styles = {".wy-nav-content": {"max-width": (px(1200), IMPORTANT)}} print(dumps(styles)) .. code-block:: css .wy-nav-content { max-width: 1200px !important } See the :py:obj:`~.Style` object for more information on the layout. The keys can also be media at-rules, with the values mappings of property names to their values: .. code-block:: python styles = { "@media screen and (min-width: 870px)": { ".wy-nav-content": {"max-width": (px(1200), IMPORTANT)}, }, } print(dumps(styles)) .. code-block:: css @media screen and (min-width: 870px) { .wy-nav-content { max-width: 1200px !important } } :param styles: A mapping of CSS selectors to styles. :param indent: The indent to use, such as a tab (``\t``), two spaces or four spaces. :param trailing_semicolon: Whether to add a semicolon to the end of the final property. :param indent_closing_brace: :param minify: Minify the CSS. Overrides all other options. :param sort_keys: Sort dictionary keys alphabetically. :param check_circular: Check for circular references. :param none_style: Whether to represent :py:obj:`None` as ``None`` or ``none``. :return: The style sheet as a string. .. versionchanged:: 0.2.0 Added support for media at-rules. .. versionchanged:: 0.5.0 New implementation. Output may differ slightly from previous css-parser based one. .. versionchanged:: 0.6.0 Added ``none_style`` option. """ # noqa: SXL001 serializer = CSSSerializer( indent=indent, trailing_semicolon=trailing_semicolon, indent_closing_brace=indent_closing_brace, minify=minify, sort_keys=sort_keys, check_circular=check_circular, none_style=none_style, ) css = serializer.encode(styles).rstrip() if css: return css + '\n' else: return ''
[docs]def dump( styles: Mapping[str, Union[Style, Mapping]], fp: Union[PathLike, IO], *, indent: str = TAB, trailing_semicolon: Optional[bool] = False, indent_closing_brace: bool = False, minify: bool = False, sort_keys: bool = False, check_circular: bool = True, none_style: Union[Literal["none"], Literal["None"]] = "none", ) -> None: r""" Construct a style sheet from a dictionary and write it to ``fp``. .. code-block:: python styles = {".wy-nav-content": {"max-width": (px(1200), IMPORTANT)}} dump(styles, ...) .. code-block:: css .wy-nav-content { max-width: 1200px !important } See the :py:obj:`~.Style` object for more information on the layout. .. latex:clearpage:: The keys can also be media at-rules, with the values mappings of property names to their values: .. code-block:: python styles = { "@media screen and (min-width: 870px)": { ".wy-nav-content": {"max-width": (px(1200), IMPORTANT)}, }, } dump(styles, ...) .. code-block:: css @media screen and (min-width: 870px) { .wy-nav-content { max-width: 1200px !important } } :param styles: A mapping of CSS selectors to styles. :param fp: An open file handle, or the filename of a file to write to. :param indent: The indent to use, such as a tab (``\t``), two spaces or four spaces. :param trailing_semicolon: Whether to add a semicolon to the end of the final property. :param indent_closing_brace: :param minify: Minify the CSS. Overrides all other options. :param sort_keys: Sort dictionary keys alphabetically. :param check_circular: Check for circular references. :param none_style: Whether to represent :py:obj:`None` as ``None`` or ``none``. .. versionchanged:: 0.2.0 * ``fp`` now accepts :py:obj:`domdf_python_tools.typing.PathLike` objects, representing the path of a file to write to. * Added support for media at-rules. .. versionchanged:: 0.5.0 New implementation. Output may differ slightly from previous css-parser based one. .. versionchanged:: 0.6.0 Added ``none_style`` option. """ # noqa: SXL001 css = dumps( styles, indent=indent, trailing_semicolon=trailing_semicolon, indent_closing_brace=indent_closing_brace, sort_keys=sort_keys, check_circular=check_circular, minify=minify, none_style=none_style, ) if isinstance(fp, TextIOBase): fp.write(css) else: PathPlus(fp).write_clean(css)
[docs]def loads(styles: str) -> MutableMapping[str, MutableMapping[str, Any]]: r""" Parse a style sheet and return its dictionary representation. .. versionadded:: 0.2.0 .. versionchanged:: 0.5.0 New implementation. Output may differ slightly from previous css-parser based one. :param styles: :return: The style sheet as a dictionary. .. latex:clearpage:: """ stylesheet = tinycss2.parse_blocks_contents(styles, skip_comments=True, skip_whitespace=True) styles_dict: MutableMapping[str, MutableMapping[str, Any]] = {} def parse_style(style: List[tinycss2.ast.Node]) -> MutableMapping[str, Property]: style_dict: Dict[str, Property] = {} prop: Union[tinycss2.ast.ParseError, tinycss2.ast.Declaration] for prop in tinycss2.parse_declaration_list(style, skip_comments=True, skip_whitespace=True): if isinstance(prop, tinycss2.ast.ParseError): raise ValueError(prop) if prop.important: style_dict[prop.name.strip()] = (_serialize(prop.value), IMPORTANT) else: style_dict[prop.name.strip()] = _serialize(prop.value) return style_dict rule: tinycss2.ast.Node for rule in stylesheet: if isinstance(rule, tinycss2.ast.QualifiedRule): styles_dict[_serialize(rule.prelude)] = parse_style(rule.content) elif isinstance(rule, tinycss2.ast.AtRule): at_rule_styles = styles_dict[f"@{rule.at_keyword} {_serialize(rule.prelude)}"] = {} for child in tinycss2.parse_blocks_contents(rule.content, skip_comments=True, skip_whitespace=True): at_rule_styles[_serialize(child.prelude)] = parse_style(child.content) else: raise NotImplementedError(rule) return styles_dict
def _serialize(nodes: Iterable[tinycss2.ast.Node]) -> str: return tinycss2.serialize(nodes).strip()
[docs]def load(fp: Union[PathLike, IO]) -> MutableMapping[str, MutableMapping[str, Any]]: r""" Parse a cascading style sheet from the given file and return its dictionary representation. .. versionadded:: 0.2.0 .. versionchanged:: 0.5.0 New implementation. Output may differ slightly from previous css-parser based one. :param fp: An open file handle, or the filename of a file to write to. :return: The style sheet as a dictionary. """ if isinstance(fp, TextIOBase): styles = fp.read() else: styles = PathPlus(fp).read_text() return loads(styles)