fix(lint): enforce accessible SVG viewBox and title rules (#123)

* fix(lint): enforce viewBox presence and title length cap

- Add a11y check: non-decorative <svg> must carry a viewBox attribute
  with exactly four numeric tokens and positive width/height dimensions.
- Add a11y check: <title> text must be 60 characters or fewer, closing
  the gap between the CONTRIBUTING.md spec and linter enforcement.
- Update VALID_SVG in test-lint-a11y.py to include viewBox so all
  existing passing cases remain valid after the new checks land.
- Add three adversarial cases: missing-viewbox, invalid-viewbox,
  title-too-long — all verified against the gate suite (47/47 OK).

* fix(lint): accept SVG comma separators and reject non-finite viewBox values

- Re-split viewBox on [\s,]+ so comma-separated values (e.g. 0,0,800,600)
  are accepted as valid SVG
- Guard each component with math.isfinite() so values like inf and nan are
  rejected rather than passing the > 0 size check
- Add adversarial test cases: viewbox-comma-separated (pass),
  viewbox-nonfinite-width, viewbox-nonfinite-height (both fail)
- Bump plugin manifests to 2.4.1

* fix(lint): reject viewBox numbers with Python-only underscore separators

* test(lint): cover remaining viewBox cases

---------

Co-authored-by: Rupam Pal <rupam.pal@euromonitor.com>
This commit is contained in:
Cathryn Lavery
2026-08-19 19:53:43 -07:00
committed by GitHub
co-authored by Rupam Pal
parent cc2f51f3fd
commit 91fb4f243b
5 changed files with 98 additions and 5 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "diagram-design", "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.", "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": { "author": {
"name": "Cathryn Lavery", "name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery" "url": "https://github.com/cathrynlavery"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "diagram-design", "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.", "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": { "author": {
"name": "Cathryn Lavery", "name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery" "url": "https://github.com/cathrynlavery"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "diagram-design", "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.", "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": { "author": {
"name": "Cathryn Lavery", "name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery" "url": "https://github.com/cathrynlavery"
+33 -1
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import math
import re import re
import sys import sys
from dataclasses import dataclass, field 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": if (svg.attrs.get("role") or "").casefold() != "img":
add(svg.line, svg.offset, 'diagram <svg> must carry role="img"') add(svg.line, svg.offset, 'diagram <svg> must carry role="img"')
viewbox = svg.attrs.get("viewbox") # HTMLParser lowercases all attribute names
if not viewbox:
add(svg.line, svg.offset, "diagram <svg> 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() labelled_by = (svg.attrs.get("aria-labelledby") or "").split()
accessible_name_ids = labelled_by + [ accessible_name_ids = labelled_by + [
element.element_id element.element_id
@@ -402,8 +427,15 @@ def lint_accessible_svgs(text, expected_slug, allow_template_placeholders=False)
title.offset, title.offset,
"<title> must be the first child element of <svg>", "<title> must be the first child element of <svg>",
) )
if not "".join(title.text).strip(): title_text = "".join(title.text).strip()
if not title_text:
add(title.line, title.offset, "<title> must not be empty") add(title.line, title.offset, "<title> must not be empty")
elif len(title_text) > 60:
add(
title.line,
title.offset,
f"<title> must be 60 characters or fewer (got {len(title_text)})",
)
if description is None: if description is None:
add(svg.line, svg.offset, "diagram <svg> must contain a <desc>") add(svg.line, svg.offset, "diagram <svg> must contain a <desc>")
+62 -1
View File
@@ -18,7 +18,7 @@ BUILD_ICONS = ROOT / "scripts/build-icons.py"
MOTION_TEMPLATE = ROOT / "skills/diagram-design/assets/template-motion.html" MOTION_TEMPLATE = ROOT / "skills/diagram-design/assets/template-motion.html"
VALID_SVG = """\ 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"> aria-labelledby="fixture-title fixture-desc">
<title id="fixture-title">Fixture diagram</title> <title id="fixture-title">Fixture diagram</title>
<desc id="fixture-desc">Diagram showing a fixture connected to a result.</desc> <desc id="fixture-desc">Diagram showing a fixture connected to a result.</desc>
@@ -156,6 +156,67 @@ def main() -> int:
'duplicate accessible-name id="fixture-desc" is not allowed', 'duplicate accessible-name id="fixture-desc" is not allowed',
directory, directory,
) )
require_failure(
"missing-viewbox",
VALID_SVG.replace(' viewBox="0 0 800 600"', ""),
"diagram <svg> 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),
"<title> 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( require_pass(
"decorative", "decorative",
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'