Files
knoxmakers-inkscape/extensions/botbox3000/deps/inkex/properties.py
2026-01-18 01:20:18 +00:00

957 lines
31 KiB
Python

# coding=utf-8
#
# Copyright (C) 2021 Jonathan Neuhauser, jonathan.neuhauser@outlook.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
Property management and parsing, CSS cascading, default value storage
.. versionadded:: 1.2
.. data:: all_properties
A list of all properties, their parser class, and additional information
such as whether they are inheritable or can be given as presentation attributes
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import (
Dict,
List,
Optional,
TYPE_CHECKING,
cast,
)
import tinycss2
import tinycss2.ast
from .units import convert_unit
from .utils import FragmentError
from .colors import Color
if TYPE_CHECKING:
from .elements import BaseElement
from .elements import _base
TokenList = List[tinycss2.ast.Node]
def _make_number_token_dimless(token):
if isinstance(token, (tinycss2.ast.NumberToken, tinycss2.ast.PercentageToken)):
return token.value
if isinstance(token, tinycss2.ast.DimensionToken):
return convert_unit(token.serialize(), "px")
raise ValueError("Not a number")
def _get_tokens_from_value(value: str) -> TokenList:
return tinycss2.parse_one_declaration(f"a:{value}").value
def _is_ws(token: tinycss2.ast.Node) -> bool:
return isinstance(token, tinycss2.ast.WhitespaceToken)
def _strip_whitespace_nodes(value: TokenList) -> TokenList:
try:
while _is_ws(value[0]):
del value[0]
while _is_ws(value[-1]):
del value[-1]
return value
except IndexError:
return []
def _is_inherit(value: TokenList | None) -> bool:
return (
value is not None
and len(value) == 1
and isinstance(value[0], tinycss2.ast.IdentToken)
and value[0].value == "inherit"
)
class _StyleConverter:
"""Converter between str and computed value of a style, with context element"""
# pylint: disable=unused-argument
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
"""Get parsed property value with resolved urls, color, etc.
Args:
element (BaseElement): the SVG element to which this style is applied to
currently used for resolving gradients / masks, could be used for
computing percentage attributes or calc() attributes [optional]
Returns:
object: parsed property value
"""
return tinycss2.serialize(value)
def raise_invalid_value(
self, value: TokenList, element: Optional[BaseElement] = None
) -> None:
"""Checks if the value str is valid in the context of element.
Args:
value (str): attribute value
element (Optional[BaseElement], optional): Context element. Defaults to
None.
Raises:
various exceptions if the property has a bad value
"""
self.convert(value, element)
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
"""Converts value back to string in the context of element.
Args:
value (object): parsed value of attribute
element (_type_, optional): Context element. Defaults to None.
Returns:
str: _description_
"""
return _get_tokens_from_value(str(value))
class _AlphaValueConverter(_StyleConverter):
"""Stores an alpha value (such as opacity), which may be specified as
as percentage or absolute value.
Reference: https://www.w3.org/TR/css-color/#typedef-alpha-value"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
if isinstance(value[0], tinycss2.ast.NumberToken):
parsed_value = value[0].value
elif isinstance(value[0], tinycss2.ast.PercentageToken):
parsed_value = value[0].value / 100
else:
raise ValueError()
return min(max(parsed_value, 0), 1)
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
if isinstance(value, (float, int)):
value = min(max(value, 0), 1)
return [tinycss2.ast.NumberToken(0, 0, value, value, f"{value}")]
raise ValueError("Value must be number")
class _ColorValueConverter(_StyleConverter):
"""Converts a color value
Reference: https://drafts.csswg.org/css-color-3/#valuea-def-color"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
# Process this as string
vstr = _StyleConverter.convert(self, value, element)
if vstr == "currentColor":
if element is not None:
return element.get_computed_style("color")
return None
return Color(vstr)
class _URLNoneValueConverter(_StyleConverter):
"""Converts a value that is either none or an url, such as markers or masks.
Reference: https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
if len(value) == 0:
return None
value = value[0]
if isinstance(value, tinycss2.ast.URLToken):
if element is not None and self.element_has_root(element):
return element.root.getElementById(value.value)
return value.serialize()
if isinstance(value, tinycss2.ast.IdentToken):
return None
raise ValueError("Invalid property value")
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
if isinstance(value, _base.BaseElement):
if element is not None:
value = _URLNoneValueConverter._insert_if_necessary(element, value)
return [tinycss2.ast.URLToken(0, 0, value.get_id(), value.get_id(as_url=2))]
return [tinycss2.ast.IdentToken(0, 0, "none")]
@staticmethod
def element_has_root(element) -> bool:
"Checks if an element has a root"
try:
_ = element.root
return True
except FragmentError:
return False
@staticmethod
def _insert_if_necessary(element: BaseElement, value: BaseElement) -> BaseElement:
"""Ensures that the return (value or a deep copy of it) is inside the same
document as element.
if element is unrooted, don't do anything (just return value)
If element is attached to the same document as value, return value.
If value is not attached to a document, attach it to the defs of element's
document and return value.
If value is already attached to another document, create a copy and return the
copy.
.. versionadded:: 1.4"""
# Check if the element is rooted and has the same root as self.element.
try:
_ = element.root
try:
if value.root != element.root:
# Create a copy and attach it to the tree.
copy = value.copy()
element.root.defs.append(copy)
return copy
except FragmentError:
element.root.defs.append(value)
return value
except FragmentError:
pass
return value
class _PaintValueConverter(_ColorValueConverter, _URLNoneValueConverter):
"""Stores a paint value (such as fill and stroke), which may be specified
as color, or url.
Reference: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
v0 = value[0]
if isinstance(v0, tinycss2.ast.IdentToken):
if v0.value == "none":
return None
if v0.value == "currentColor":
return _ColorValueConverter.convert(self, value, element)
if v0.value in ["context-fill", "context-stroke"]:
return v0.value
else:
return Color(v0.value)
if isinstance(v0, tinycss2.ast.HashToken):
return Color("#" + v0.value)
if isinstance(v0, tinycss2.ast.URLToken):
if element is not None and self.element_has_root(element):
paint_server = element.root.getElementById(v0.value[1:])
else:
return None
if paint_server is not None:
return paint_server
for i in value[1:]:
if isinstance(i, tinycss2.ast.IdentToken):
return Color(i.value)
raise ValueError("Paint server not found")
if isinstance(v0, tinycss2.ast.FunctionBlock) and v0.name in [
"rgb",
"rgba",
"hsl",
"hsla",
]:
arguments = [str(argument.value) for argument in v0.arguments]
return Color(f"{v0.name}({''.join(arguments)})")
raise ValueError("Unknown color specification")
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
if value is None:
return [tinycss2.ast.IdentToken(0, 0, "none")]
if isinstance(value, _base.BaseElement):
return _URLNoneValueConverter.convert_back(self, value, element=element)
return _ColorValueConverter.convert_back(self, value, element=element)
class _EnumValueConverter(_StyleConverter):
"""Stores a value that can only have a finite set of options"""
def __init__(self, options):
self.options = options
def raise_invalid_value(
self, value: TokenList, element: BaseElement | None = None
) -> None:
if tinycss2.serialize(value) not in self.options:
raise ValueError(
f"Value '{tinycss2.serialize(value)}' is invalid for the property"
)
class _ShorthandValueConverter(_StyleConverter):
"""Stores a value that sets other values (e.g. the font shorthand)"""
def __init__(self, keys) -> None:
super().__init__()
self.keys = keys
@abstractmethod
def get_shorthand_changes(self, value) -> Dict[str, str]:
"""calculates the value of affected attributes for this shorthand
Returns:
Dict[str, str]: a dictionary containing the new values of the
affected attributes
"""
class _FontValueShorthandConverter(_ShorthandValueConverter):
"""Logic for the shorthand font property"""
def __init__(self) -> None:
super().__init__(
[
"font-style",
"font-variant",
"font-weight",
"font-stretch",
"font-size",
"line-height",
"font-family",
]
)
self.options = {
key: all_properties[key].converter.options # type: ignore
for key in self.keys
if isinstance(all_properties[key].converter, _EnumValueConverter)
}
# Font stretch can be specified in percent, but for the
# shorthand, only a keyword value is allowed
self.options["font-stretch"] = (
"normal",
"ultra-condensed",
"extra-condensed",
"condensed",
"semi-condensed",
"semi-expanded",
"expanded",
"extra-expanded",
"ultra-expanded",
)
def get_shorthand_changes(self, value):
result = {key: all_properties[key].default_value for key in self.keys}
if len(value) == 0:
return result # shorthand not set, nothing to do
i = 0
while i < len(value):
cur = value[i]
matched = False
if isinstance(cur, tinycss2.ast.IdentToken):
matched = True
if cur.value in self.options["font-style"]:
result["font-style"] = [cur]
elif cur.value in self.options["font-variant"]:
result["font-variant"] = [cur]
elif cur.value in self.options["font-weight"]:
result["font-weight"] = [cur]
elif cur.value in self.options["font-stretch"]:
result["font-stretch"] = [cur]
else:
matched = False
if not matched and not isinstance(cur, tinycss2.ast.WhitespaceToken):
result["font-size"] = [cur]
if (
len(value) > i + 1
and isinstance(value[i + 1], tinycss2.ast.LiteralToken)
and value[i + 1].value == "/"
):
result["line-height"] = [value[i + 2]]
i += 2
if len(value[i + 1 :]) > 0:
result["font-family"] = _strip_whitespace_nodes(value[i + 1 :])
break
i += 1
return result
class _TextDecorationValueConverter(_ShorthandValueConverter):
"""Logic for the shorthand font property
.. versionadded:: 1.3"""
def __init__(self):
super().__init__(
["text-decoration-style", "text-decoration-color", "text-decoration-line"]
)
self.options = {
"text-decoration-" + key: all_properties[
"text-decoration-" + key
].converter.options
for key in ("line", "style", "color")
if isinstance(
all_properties["text-decoration-" + key].converter, _EnumValueConverter
)
}
def get_shorthand_changes(self, value):
result = {
"text-decoration-style": all_properties[
"text-decoration-style"
].default_value,
"text-decoration-color": _get_tokens_from_value("currentcolor"),
"text-decoration-line": [],
}
for token, cur in list((i, i.serialize()) for i in value):
if cur in ["underline", "overline", "line-through", "blink"]:
result["text-decoration-line"].extend(
[token, tinycss2.ast.WhitespaceToken(0, 0, value=" ")]
)
elif cur in self.options["text-decoration-style"]:
result["text-decoration-style"] = [token]
elif cur.strip():
result["text-decoration-color"] = [token]
if len(result["text-decoration-line"]) == 0:
result["text-decoration-line"] = all_properties[
"text-decoration-line"
].default_value
else:
result["text-decoration-line"] = result["text-decoration-line"][:-1]
return result
class _MarkerShorthandValueConverter(_ShorthandValueConverter, _URLNoneValueConverter):
"""Logic for the marker shorthand property"""
def __init__(self) -> None:
super().__init__(["marker-start", "marker-end", "marker-mid"])
def get_shorthand_changes(self, value):
if value == "":
return {} # shorthand not set, nothing to do
return {k: value for k in self.keys}
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
"""Convert value back to a tokenlist"""
return _URLNoneValueConverter.convert_back(self, value, element)
class _FontSizeValueConverter(_StyleConverter):
"""Logic for the font-size property"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
v0 = value[0].serialize()
if element is None:
return v0 # no additional logic in this case
if isinstance(
value[0],
(
tinycss2.ast.NumberToken,
tinycss2.ast.PercentageToken,
tinycss2.ast.DimensionToken,
),
):
return element.to_dimensionless(v0)
# unable to parse font size, e.g. font-size:normal
return v0
class _StrokeDasharrayValueConverter(_StyleConverter):
"""Logic for the stroke-dasharray property"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
dashes = []
i = 0
for i, el in enumerate(value):
if (
i == 0
and isinstance(el, tinycss2.ast.IdentToken)
and el.value == "none"
):
return []
# whitespace or comma separated list
if isinstance(el, (tinycss2.ast.WhitespaceToken)) or (
isinstance(el, tinycss2.ast.LiteralToken) and el.value == ","
):
continue
if isinstance(el, (tinycss2.ast.DimensionToken, tinycss2.ast.NumberToken)):
dashes.append(_make_number_token_dimless(el))
else:
return []
if any(i < 0 for i in dashes):
# one negative value makes the dasharray invalid
return []
if len(dashes) % 2 == 1:
dashes = 2 * dashes
return dashes
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
if value is None or not value:
value = "none"
if isinstance(value, list):
if len(value) == 0:
value = "none"
else:
value = " ".join(map(str, value))
return _get_tokens_from_value(str(value))
class _FilterListConverter(_URLNoneValueConverter):
"""Stores a list of Filters
.. versionadded:: 1.4"""
def convert(
self, value: TokenList, element: Optional[BaseElement] = None
) -> object:
if element is None or value == "none":
return []
try:
result = [
element.root.getElementById(item.value)
for item in value
if isinstance(item, tinycss2.ast.URLToken)
]
return [i for i in result if i is not None]
except FragmentError:
return [
item.serialize()
for item in value
if isinstance(item, tinycss2.ast.URLToken)
]
except ValueError: # broken link
return []
def convert_back(
self, value: object, element: Optional[BaseElement] = None
) -> TokenList:
if isinstance(value, _base.BaseElement) or not isinstance(value, (list, tuple)):
value = [value]
if all((isinstance(i, str) for i in value)):
return _get_tokens_from_value(" ".join(value)) # type: ignore
assert element is not None
value = cast("List[BaseElement]", value)
try:
_ = element.root
for i in value:
if i is None:
continue
# insert the element
inserted = self._insert_if_necessary(element, cast("BaseElement", i))
# if a copy was created, replace the original in the list with the copy
if inserted is not i:
for index, item in enumerate(value):
if item is i:
value[index] = inserted
except FragmentError:
# Element is unrooted, we skip this step.
pass
return _get_tokens_from_value(
" ".join(f"url(#{i.get_id()})" for i in value if i is not None)
)
@dataclass
class PropertyDescription:
"""Describes a CSS / presentation attribute"""
name: str
converter: _StyleConverter
default_value: TokenList
presentation: bool
inherited: bool
def __init__(
self,
name: str,
converter: _StyleConverter,
default_value: str,
presentation: bool = False,
inherited: bool = False,
):
self.presentation = presentation
self.inherited = inherited
self.default_value = _get_tokens_from_value(default_value)
self.name = name
self.converter = converter
# Source for this list: https://www.w3.org/TR/SVG2/styling.html#PresentationAttributes
properties_list: List[PropertyDescription] = [
PropertyDescription(
"alignment-baseline",
_EnumValueConverter(
[
"baseline",
"text-bottom",
"alphabetic",
"ideographic",
"middle",
"central",
"mathematical",
"text-top",
]
),
"baseline",
True,
False,
),
PropertyDescription("baseline-shift", _StyleConverter(), "0", True, False),
PropertyDescription("clip", _StyleConverter(), "auto", True, False),
PropertyDescription("clip-path", _URLNoneValueConverter(), "none", True, False),
PropertyDescription(
"clip-rule", _EnumValueConverter(["nonzero", "evenodd"]), "nonzero", True, True
),
PropertyDescription("color", _PaintValueConverter(), "black", True, True),
PropertyDescription(
"color-interpolation",
_EnumValueConverter(["sRGB", "auto", "linearRGB"]),
"sRGB",
True,
True,
),
PropertyDescription(
"color-interpolation-filters",
_EnumValueConverter(["auto", "sRGB", "linearRGB"]),
"linearRGB",
True,
True,
),
PropertyDescription(
"color-rendering",
_EnumValueConverter(
["auto", "optimizeSpeed", "optimizeQuality", "pixelated", "crisp-edges"]
),
"auto",
True,
True,
),
PropertyDescription("cursor", _StyleConverter(), "auto", True, True),
PropertyDescription(
"direction", _EnumValueConverter(["ltr", "rtl"]), "ltr", True, True
),
PropertyDescription(
"display",
_EnumValueConverter(
[
"inline",
"block",
"list-item",
"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",
]
),
"inline",
True,
False,
),
PropertyDescription(
"dominant-baseline",
_EnumValueConverter(
[
"auto",
"text-bottom",
"alphabetic",
"ideographic",
"middle",
"central",
"mathematical",
"hanging",
"text-top",
]
),
"auto",
True,
True,
),
PropertyDescription("fill", _PaintValueConverter(), "black", True, True),
PropertyDescription("fill-opacity", _AlphaValueConverter(), "1", True, True),
PropertyDescription(
"fill-rule", _EnumValueConverter(["nonzero", "evenodd"]), "nonzero", True, True
),
PropertyDescription("filter", _FilterListConverter(), "none", True, False),
PropertyDescription("flood-color", _PaintValueConverter(), "black", True, False),
PropertyDescription("flood-opacity", _AlphaValueConverter(), "1", True, False),
PropertyDescription("font-family", _StyleConverter(), "sans-serif", True, True),
PropertyDescription("font-size", _FontSizeValueConverter(), "medium", True, True),
PropertyDescription("font-size-adjust", _StyleConverter(), "none", True, True),
PropertyDescription("font-stretch", _StyleConverter(), "normal", True, True),
PropertyDescription(
"font-style",
_EnumValueConverter(["normal", "italic", "oblique"]),
"normal",
True,
True,
),
PropertyDescription(
"font-variant",
_EnumValueConverter(["normal", "small-caps"]),
"normal",
True,
True,
),
PropertyDescription(
"font-weight",
_EnumValueConverter(
["normal", "bold"] + [str(i) for i in range(100, 901, 100)]
),
"normal",
True,
True,
),
PropertyDescription(
"glyph-orientation-horizontal", _StyleConverter(), "0deg", True, True
),
PropertyDescription(
"glyph-orientation-vertical", _StyleConverter(), "auto", True, True
),
PropertyDescription("inline-size", _StyleConverter(), "0", False, False),
PropertyDescription(
"image-rendering",
_EnumValueConverter(["auto", "optimizeQuality", "optimizeSpeed"]),
"auto",
True,
True,
),
PropertyDescription("letter-spacing", _StyleConverter(), "normal", True, True),
PropertyDescription(
"lighting-color", _ColorValueConverter(), "normal", True, False
),
PropertyDescription("line-height", _StyleConverter(), "normal", False, True),
PropertyDescription("marker", _MarkerShorthandValueConverter(), ""),
PropertyDescription("marker-end", _URLNoneValueConverter(), "none", True, True),
PropertyDescription("marker-mid", _URLNoneValueConverter(), "none", True, True),
PropertyDescription("marker-start", _URLNoneValueConverter(), "none", True, True),
PropertyDescription("mask", _URLNoneValueConverter(), "none", True, False),
PropertyDescription("opacity", _AlphaValueConverter(), "1", True, False),
PropertyDescription(
"overflow",
_EnumValueConverter(["visible", "hidden", "scroll", "auto"]),
"visible",
True,
False,
),
PropertyDescription("paint-order", _StyleConverter(), "normal", True, False),
PropertyDescription(
"pointer-events",
_EnumValueConverter(
[
"bounding-box",
"visiblePainted",
"visibleFill",
"visibleStroke",
"visible",
"painted",
"fill",
"stroke",
"all",
"none",
]
),
"visiblePainted",
True,
True,
),
PropertyDescription("shape-inside", _URLNoneValueConverter(), "none", False, False),
PropertyDescription(
"shape-rendering",
_EnumValueConverter(
["auto", "optimizeSpeed", "crispEdges", "geometricPrecision"]
),
"visiblePainted",
True,
True,
),
PropertyDescription("stop-color", _ColorValueConverter(), "black", True, False),
PropertyDescription("stop-opacity", _AlphaValueConverter(), "1", True, False),
PropertyDescription("stroke", _PaintValueConverter(), "none", True, True),
PropertyDescription(
"stroke-dasharray", _StrokeDasharrayValueConverter(), "none", True, True
),
PropertyDescription("stroke-dashoffset", _StyleConverter(), "0", True, True),
PropertyDescription(
"stroke-linecap",
_EnumValueConverter(["butt", "round", "square"]),
"butt",
True,
True,
),
PropertyDescription(
"stroke-linejoin",
_EnumValueConverter(["miter", "miter-clip", "round", "bevel", "arcs"]),
"miter",
True,
True,
),
PropertyDescription("stroke-miterlimit", _StyleConverter(), "4", True, True),
PropertyDescription("stroke-opacity", _AlphaValueConverter(), "1", True, True),
PropertyDescription("stroke-width", _StyleConverter(), "1", True, True),
PropertyDescription("text-align", _StyleConverter(), "start", True, True),
PropertyDescription(
"text-anchor",
_EnumValueConverter(["start", "middle", "end"]),
"start",
True,
True,
),
PropertyDescription(
"text-decoration-line", _StyleConverter(), "none", False, False
),
PropertyDescription(
"text-decoration-style",
_EnumValueConverter(["solid", "double", "dotted", "dashed", "wavy"]),
"solid",
False,
False,
),
PropertyDescription(
"text-decoration-color", _StyleConverter(), "currentcolor", False, False
),
PropertyDescription(
"text-overflow", _EnumValueConverter(["clip", "ellipsis"]), "clip", True, False
),
PropertyDescription(
"text-rendering",
_EnumValueConverter(
["auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision"]
),
"auto",
True,
True,
),
PropertyDescription(
"unicode-bidi",
_EnumValueConverter(
[
"normal",
"embed",
"isolate",
"bidi-override",
"isolate-override",
"plaintext",
]
),
"normal",
True,
False,
),
PropertyDescription("vector-effect", _StyleConverter(), "none", True, False),
PropertyDescription("vertical-align", _StyleConverter(), "baseline", False, False),
PropertyDescription(
"visibility",
_EnumValueConverter(["visible", "hidden", "collapse"]),
"visible",
True,
True,
),
PropertyDescription(
"white-space",
_EnumValueConverter(
["normal", "pre", "nowrap", "pre-wrap", "break-spaces", "pre-line"]
),
"normal",
True,
True,
),
PropertyDescription("word-spacing", _StyleConverter(), "normal", True, True),
PropertyDescription(
"writing-mode",
_EnumValueConverter(
[
"horizontal-tb",
"vertical-rl",
"vertical-lr",
"lr",
"lr-tb",
"rl",
"rl-tb",
"tb",
"tb-rl",
]
),
"horizontal-tb",
True,
True,
),
PropertyDescription(
"-inkscape-font-specification", _StyleConverter(), "sans-serif", False, True
),
]
all_properties = {v.name: v for v in properties_list}
properties_list += [
PropertyDescription("font", _FontValueShorthandConverter(), ""),
PropertyDescription("text-decoration", _TextDecorationValueConverter(), ""),
]
all_properties["font"] = properties_list[-2]
all_properties["text-decoration"] = properties_list[-1]
shorthand_from_value = {
item: prop.name
for prop in properties_list
if isinstance(prop.converter, _ShorthandValueConverter)
for item in prop.converter.keys
}
shorthand_properties = {
i.name: i
for i in properties_list
if isinstance(i.converter, _ShorthandValueConverter)
}