diff --git a/pixi.lock b/pixi.lock index 2ae0b47..37011eb 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1454,11 +1454,11 @@ packages: name: ewoksdraw requires_dist: - defusedxml + - ewokscore==5.1.0rc3 - reportlab - typing-extensions ; python_full_version < '3.11' - xmltodict - pyelk @ git+https://github.com/LudoBroche/pyelk.git@ewoksdraw-modifications - - ewokscore==5.1.0rc3 - pytest>=7 ; extra == 'test' - ewoksdraw[test] ; extra == 'dev' - ruff>=0.16.0 ; extra == 'dev' diff --git a/src/ewoksdraw/svg/utils.py b/src/ewoksdraw/svg/utils.py index db523f5..1e2e8be 100644 --- a/src/ewoksdraw/svg/utils.py +++ b/src/ewoksdraw/svg/utils.py @@ -2,15 +2,42 @@ from xml.etree.ElementTree import Element CSS_DIR = Path(__file__).parent.parent / "css_styles" +ROOT_CSS_PATH = CSS_DIR / "root.css" -def generate_style_element(css_file_name: str) -> None | Element: - css_file_path = CSS_DIR / css_file_name - if not css_file_path.exists(): - return None - - with open(css_file_path, "r") as css_file: - css_content = css_file.read() +def generate_style_element(css_file_name: str) -> Element: + css = (CSS_DIR / css_file_name).read_text() + css_with_fallback = _add_css_variable_fallback(css) style = Element("style") - style.text = f"" + style.text = f"" return style + + +def _add_css_variable_fallback(css: str) -> str: + + if "var(" not in css: + return css + + theme_values = _read_theme_values() + css_with_plain_values = _replace_css_variables(css, theme_values) + original_css_if_supported = f"@supports (--css: variables) {{\n{css}\n}}" + return f"{css_with_plain_values}\n{original_css_if_supported}" + + +def _read_theme_values() -> dict[str, str]: + """Read root.css into {"--link-color": "rgb(176, 147, 255)", ...}.""" + root_css = ROOT_CSS_PATH.read_text() + theme_values: dict[str, str] = {} + for line in root_css.splitlines(): + declaration = line.strip().removesuffix(";") + if declaration.startswith("--"): + name, value = declaration.split(": ") + theme_values[name] = value + return theme_values + + +def _replace_css_variables(css: str, theme_values: dict[str, str]) -> str: + """Replace each "var(--link-color)" by its value, e.g. "rgb(176, 147, 255)".""" + for name, value in theme_values.items(): + css = css.replace(f"var({name})", value) + return css diff --git a/src/ewoksdraw/tests/test_svg_css_variables.py b/src/ewoksdraw/tests/test_svg_css_variables.py new file mode 100644 index 0000000..f35ef45 --- /dev/null +++ b/src/ewoksdraw/tests/test_svg_css_variables.py @@ -0,0 +1,63 @@ +from pathlib import Path + +import pytest + +from ewoksdraw.svg import utils + + +@pytest.fixture +def css_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + (tmp_path / "root.css").write_text( + ".ewoksdraw {\n" + " --link-color: #b093ff;\n" + " --stroke-color: #ffffff;\n" + " --error: #ff0000;\n" + "}\n" + ) + monkeypatch.setattr(utils, "ROOT_CSS_PATH", tmp_path / "root.css") + return tmp_path + + +def test_variable_generates_fallback_and_supports(css_dir: Path) -> None: + result = utils._add_css_variable_fallback(".anchor { fill: var(--link-color); }") + + assert " ".join(result.split()) == ( + ".anchor { fill: #b093ff; } " + "@supports (--css: variables) { " + ".anchor { fill: var(--link-color); } }" + ) + + +def test_css_without_variables_is_unchanged(css_dir: Path) -> None: + css = ".anchor {\n fill: purple;\n stroke-width: 2;\n}\n" + + assert utils._add_css_variable_fallback(css) == css + + +def test_multiple_variables_are_replaced(css_dir: Path) -> None: + result = utils._add_css_variable_fallback( + ".anchor { fill: var(--link-color); " + "stroke: var(--stroke-color); stroke-width: 2; }" + ) + + assert " ".join(result.split()) == ( + ".anchor { fill: #b093ff; stroke: #ffffff; stroke-width: 2; } " + "@supports (--css: variables) { " + ".anchor { fill: var(--link-color); " + "stroke: var(--stroke-color); stroke-width: 2; } }" + ) + + +def test_import_error_rule_stays_after_normal_rule(css_dir: Path) -> None: + result = utils._add_css_variable_fallback( + ".task_box { stroke: var(--stroke-color); }\n" + ".task_box[data-import-error=''] { stroke: var(--error); }" + ) + + assert " ".join(result.split()) == ( + ".task_box { stroke: #ffffff; } " + ".task_box[data-import-error=''] { stroke: #ff0000; } " + "@supports (--css: variables) { " + ".task_box { stroke: var(--stroke-color); } " + ".task_box[data-import-error=''] { stroke: var(--error); } }" + ) diff --git a/src/ewoksdraw/tests/test_utils.py b/src/ewoksdraw/tests/test_utils.py index d7334b5..662a949 100644 --- a/src/ewoksdraw/tests/test_utils.py +++ b/src/ewoksdraw/tests/test_utils.py @@ -4,7 +4,7 @@ from ..utils import get_task_id_from_target_id -def test_edge_source_task_id_roundtrip(): +def test_edge_source_task_id_roundtrip() -> None: task_id = "pipeline.output.normalize" output_name = "value" @@ -12,7 +12,7 @@ def test_edge_source_task_id_roundtrip(): assert get_task_id_from_source_id(source_id) == task_id -def test_edge_target_task_id_roundtrip(): +def test_edge_target_task_id_roundtrip() -> None: task_id = "pipeline.input.normalize" output_name = "value"