diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index addb18f..c3f063b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.5.15", + "version": "2.5.16", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 23d6fba..93cdbac 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.5.15", + "version": "2.5.16", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/.factory-plugin/plugin.json b/.factory-plugin/plugin.json index addb18f..c3f063b 100644 --- a/.factory-plugin/plugin.json +++ b/.factory-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.5.15", + "version": "2.5.16", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/scripts/lint-skin.py b/scripts/lint-skin.py index 22cfeab..6b40127 100644 --- a/scripts/lint-skin.py +++ b/scripts/lint-skin.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse import hashlib +import math import re import sys from dataclasses import dataclass, field @@ -349,6 +350,30 @@ def lint_accessible_svgs(text, expected_slug, allow_template_placeholders=False) if (svg.attrs.get("role") or "").casefold() != "img": add(svg.line, svg.offset, 'diagram must carry role="img"') + viewbox = svg.attrs.get("viewbox") # HTMLParser lowercases all attribute names + if not viewbox: + add(svg.line, svg.offset, "diagram must have a viewBox attribute") + else: + parts = re.split(r"[\s,]+", viewbox.strip()) + valid = False + _svg_num_re = re.compile(r"^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$") + if len(parts) == 4 and all(_svg_num_re.match(p) for p in parts): + try: + nums = [float(p) for p in parts] + valid = ( + all(math.isfinite(n) for n in nums) + and nums[2] > 0 + and nums[3] > 0 + ) + except ValueError: + pass + if not valid: + add( + svg.line, + svg.offset, + f'viewBox "{viewbox}" is not valid (expected "min-x min-y width height" with positive size)', + ) + labelled_by = (svg.attrs.get("aria-labelledby") or "").split() accessible_name_ids = labelled_by + [ element.element_id @@ -402,8 +427,15 @@ def lint_accessible_svgs(text, expected_slug, allow_template_placeholders=False) title.offset, " must be the first child element of ", ) - if not "".join(title.text).strip(): + title_text = "".join(title.text).strip() + if not title_text: add(title.line, title.offset, " must not be empty") + elif len(title_text) > 60: + add( + title.line, + title.offset, + f" must be 60 characters or fewer (got {len(title_text)})", + ) if description is None: add(svg.line, svg.offset, "diagram <svg> must contain a <desc>") diff --git a/scripts/test-lint-a11y.py b/scripts/test-lint-a11y.py index f3c6663..0103395 100644 --- a/scripts/test-lint-a11y.py +++ b/scripts/test-lint-a11y.py @@ -18,7 +18,7 @@ BUILD_ICONS = ROOT / "scripts/build-icons.py" MOTION_TEMPLATE = ROOT / "skills/diagram-design/assets/template-motion.html" VALID_SVG = """\ -<svg xmlns="http://www.w3.org/2000/svg" role="img" +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" role="img" aria-labelledby="fixture-title fixture-desc"> <title id="fixture-title">Fixture diagram Diagram showing a fixture connected to a result. @@ -156,6 +156,67 @@ def main() -> int: 'duplicate accessible-name id="fixture-desc" is not allowed', directory, ) + require_failure( + "missing-viewbox", + VALID_SVG.replace(' viewBox="0 0 800 600"', ""), + "diagram must have a viewBox attribute", + directory, + ) + require_failure( + "invalid-viewbox", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0 0 -10 100"'), + 'viewBox "0 0 -10 100" is not valid (expected "min-x min-y width height" with positive size)', + directory, + ) + require_failure( + "viewbox-zero-height", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0 0 800 0"'), + 'viewBox "0 0 800 0" is not valid (expected "min-x min-y width height" with positive size)', + directory, + ) + require_failure( + "viewbox-malformed-token", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0 0 800px 600"'), + 'viewBox "0 0 800px 600" is not valid (expected "min-x min-y width height" with positive size)', + directory, + ) + require_failure( + "title-too-long", + VALID_SVG.replace("Fixture diagram", "A" * 61), + " must be 60 characters or fewer (got 61)", + directory, + ) + require_pass( + "viewbox-comma-separated", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0,0,800,600"').replace( + "fixture-title", "viewbox-comma-separated-title" + ).replace("fixture-desc", "viewbox-comma-separated-desc"), + directory, + ) + require_failure( + "viewbox-nonfinite-width", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0 0 inf 600"').replace( + "fixture-title", "viewbox-nonfinite-width-title" + ).replace("fixture-desc", "viewbox-nonfinite-width-desc"), + 'viewBox "0 0 inf 600" is not valid (expected "min-x min-y width height" with positive size)', + directory, + ) + require_failure( + "viewbox-nonfinite-height", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0 0 800 inf"').replace( + "fixture-title", "viewbox-nonfinite-height-title" + ).replace("fixture-desc", "viewbox-nonfinite-height-desc"), + 'viewBox "0 0 800 inf" is not valid (expected "min-x min-y width height" with positive size)', + directory, + ) + require_failure( + "viewbox-underscore-width", + VALID_SVG.replace('viewBox="0 0 800 600"', 'viewBox="0 0 1_000 600"').replace( + "fixture-title", "viewbox-underscore-width-title" + ).replace("fixture-desc", "viewbox-underscore-width-desc"), + 'viewBox "0 0 1_000 600" is not valid (expected "min-x min-y width height" with positive size)', + directory, + ) require_pass( "decorative", '