<commit_before>from typing import Dict, Optional, Sequence, Tuple, Union

import tinycss2.color3
from typing_extensions import TypedDict

from gaphor.core.styling import StyleDeclarations
from gaphor.diagram.text import (
    FontStyle,
    FontWeight,
    TextAlign,
    TextDecoration,
    VerticalAlign,
)

Color = Tuple[float, float, float, float]  # RGBA
Padding = Tuple[float, float, float, float]  # top/right/bottom/left
Number = Union[int, float]

# Style is using SVG properties where possible
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
Style = TypedDict(
    "Style",
    {
        "padding": Padding,
        "min-width": Number,
        "min-height": Number,
        "line-width": Number,
        "vertical-spacing": Number,
        "border-radius": Number,
        "background-color": Color,
        "font-family": str,
        "font-size": Number,
        "font-style": FontStyle,
        "font-weight": FontWeight,
        "text-decoration": TextDecoration,
        "text-align": TextAlign,
        "text-color": Color,
        "color": Color,
        "vertical-align": VerticalAlign,
        "dash-style": Sequence[Number],
        "highlight-color": Color,
    },
    total=False,
)


# Not all styles are requires: "background-color", "font-weight",
# "text-color", and "text-decoration" are optional (can default to None)
DEFAULT_STYLE: Style = {
    "min-width": 0,
    "min-height": 0,
    "padding": (0, 0, 0, 0),
    "vertical-align": VerticalAlign.MIDDLE,
    "vertical-spacing": 4,
    "border-radius": 0,
    "padding": (0, 0, 0, 0),
    "line-width": 2,
    "dash-style": [],
    "color": (0, 0, 0, 1),
    "font-family": "sans",
    "font-size": 14,
    "font-style": FontStyle.NORMAL,
    "text-align": TextAlign.CENTER,
    "highlight-color": (0, 0, 1, 0.4),
}


def combined_style(item_style: Style, inline_style: Style = {}) -> Style:
    """
    Combine default style, context style and inline styles into one style.
    """
    return {**DEFAULT_STYLE, **item_style, **inline_style}  # type: ignore[misc]


number = (int, float)


def _clip_color(c):
    if c < 0:
        return 0
    if c > 1:
        return 1
    return c


@StyleDeclarations.register(
    "background-color", "color", "highlight-color", "text-color"
)
def parse_color(prop, value):
    color = tinycss2.color3.parse_color(value)
    return tuple(_clip_color(v) for v in color) if color else None


@StyleDeclarations.register(
    "min-width",
    "min-height",
    "line-width",
    "vertical-spacing",
    "border-radius",
    "font-size",
)
def parse_positive_number(prop, value) -> Optional[Number]:
    if isinstance(value, number) and value > 0:
        return value
    return None


@StyleDeclarations.register("font-family")
def parse_string(prop, value) -> Optional[str]:<commit_msg>Do not throw exception on invalid color<commit_after>from typing import Dict, Optional, Sequence, Tuple, Union

import tinycss2.color3
from typing_extensions import TypedDict

from gaphor.core.styling import StyleDeclarations
from gaphor.diagram.text import (
    FontStyle,
    FontWeight,
    TextAlign,
    TextDecoration,
    VerticalAlign,
)

Color = Tuple[float, float, float, float]  # RGBA
Padding = Tuple[float, float, float, float]  # top/right/bottom/left
Number = Union[int, float]

# Style is using SVG properties where possible
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
Style = TypedDict(
    "Style",
    {
        "padding": Padding,
        "min-width": Number,
        "min-height": Number,
        "line-width": Number,
        "vertical-spacing": Number,
        "border-radius": Number,
        "background-color": Color,
        "font-family": str,
        "font-size": Number,
        "font-style": FontStyle,
        "font-weight": FontWeight,
        "text-decoration": TextDecoration,
        "text-align": TextAlign,
        "text-color": Color,
        "color": Color,
        "vertical-align": VerticalAlign,
        "dash-style": Sequence[Number],
        "highlight-color": Color,
    },
    total=False,
)


# Not all styles are requires: "background-color", "font-weight",
# "text-color", and "text-decoration" are optional (can default to None)
DEFAULT_STYLE: Style = {
    "min-width": 0,
    "min-height": 0,
    "padding": (0, 0, 0, 0),
    "vertical-align": VerticalAlign.MIDDLE,
    "vertical-spacing": 4,
    "border-radius": 0,
    "padding": (0, 0, 0, 0),
    "line-width": 2,
    "dash-style": [],
    "color": (0, 0, 0, 1),
    "font-family": "sans",
    "font-size": 14,
    "font-style": FontStyle.NORMAL,
    "text-align": TextAlign.CENTER,
    "highlight-color": (0, 0, 1, 0.4),
}


def combined_style(item_style: Style, inline_style: Style = {}) -> Style:
    """
    Combine default style, context style and inline styles into one style.
    """
    return {**DEFAULT_STYLE, **item_style, **inline_style}  # type: ignore[misc]


number = (int, float)


def _clip_color(c):
    if c < 0:
        return 0
    if c > 1:
        return 1
    return c


@StyleDeclarations.register(
    "background-color", "color", "highlight-color", "text-color"
)
def parse_color(prop, value):
    try:
        color = tinycss2.color3.parse_color(value)
    except AttributeError:
        return None
    return tuple(_clip_color(v) for v in color) if color else None


@StyleDeclarations.register(
    "min-width",
    "min-height",
    "line-width",
    "vertical-spacing",
    "border-radius",
    "font-size",
)
def parse_positive_number(prop, value) -> Optional[Number]:
    if isinstance(value, number) and value > 0:
        return value
    return None


@StyleDeclarations.register("font-family")
def parse_string(prop, value) -> Optional[str]:
{'fraction_non_alphanumeric': 0.14330653617616218, 'fraction_numerical': 0.011534428521495981, 'mean_word_length': 3.0133239831697054, 'pattern_counts': {'":': 66, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3479056', 'n_tokens_mistral': 1951, 'n_tokens_neox': 1854, 'n_words': 478}