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:
co-authored by
Rupam Pal
parent
cc2f51f3fd
commit
91fb4f243b
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+33
-1
@@ -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 <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()
|
||||
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,
|
||||
"<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")
|
||||
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:
|
||||
add(svg.line, svg.offset, "diagram <svg> must contain a <desc>")
|
||||
|
||||
@@ -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</title>
|
||||
<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',
|
||||
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(
|
||||
"decorative",
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'
|
||||
|
||||
Reference in New Issue
Block a user