code wiki / _hdl_build / nx_cms_a11y.nx
nx_cms_a11y.nx source
↩ module page · 113 lines · 4356 B
1// nx_cms_a11y.nx -- CMS ACCESSIBILITY (WCAG) auditor (the wp-accessibility class, built honestly). Static
2// HTML checks against a focused, citable WCAG 2.x rule set -- the kind of automated a11y gate WordPress
3// themes routinely fail:
4// R1 WCAG 3.1.1 : the document declares a language (<html ... lang=...>)
5// R2 WCAG 1.1.1 : every <img> carries a non-empty alt= (text alternative)
6// R3 WCAG 1.3.1 : heading levels are not SKIPPED (h1 -> h3 with no h2 = fail)
7// R4 WCAG 4.1.2 : every non-hidden <input> has an accessible name (aria-label=)
8// Pure functions over an HTML buffer (defensive at the boundary, rule 12). license_tier: ORIGINAL
9// module: nishi-core.cms.a11y
10// depends: nishi-core.io.syscalls
11// capability: CMS
12import "nx_syscalls.nx"
13
14func a11_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
15
16// first offset of NUL-terminated needle in buf[from..n); -1 if absent
17func a11_find(buf: *u8, from: i64, n: i64, needle: *u8) -> i64 {
18 let nl: i64 = a11_slen(needle)
19 var i: i64 = from
20 while i + nl <= n {
21 var q: i64 = 0
22 var ok: i64 = 1
23 while q < nl { if (buf[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 }
24 if ok == 1 { return i }
25 i = i + 1
26 }
27 return 0 - 1
28}
29
30// offset of the next '>' at or after `from` (tag end), or n
31func a11_tag_end(buf: *u8, from: i64, n: i64) -> i64 {
32 var i: i64 = from
33 while i < n { if (buf[i] as i64) == 62 { return i } i = i + 1 }
34 return n
35}
36
37// R1 WCAG 3.1.1: document declares a language. find "<html", require "lang=" before that tag closes.
38func a11y_has_lang(buf: *u8, n: i64) -> i64 {
39 let h: i64 = a11_find(buf, 0, n, "<html" as *u8)
40 if h < 0 { return 0 }
41 let te: i64 = a11_tag_end(buf, h, n)
42 if a11_find(buf, h, te, "lang=" as *u8) >= 0 { return 1 }
43 return 0
44}
45
46// helper: does the attribute attr (e.g. "alt=") appear with a NON-EMPTY quoted value inside [from,end)?
47func a11_attr_nonempty(buf: *u8, from: i64, end: i64, attr: *u8) -> i64 {
48 let a: i64 = a11_find(buf, from, end, attr)
49 if a < 0 { return 0 }
50 let al: i64 = a11_slen(attr)
51 let vq: i64 = a + al // expected opening quote position
52 if vq + 1 >= end { return 0 }
53 if (buf[vq] as i64) != 34 { return 0 } // must be a double-quote
54 if (buf[vq+1] as i64) == 34 { return 0 } // empty value ("") -> fail
55 return 1
56}
57
58// R2 WCAG 1.1.1: every <img> has a non-empty alt=. returns 1 if all OK (or no imgs), 0 if any missing/empty.
59func a11y_imgs_alt(buf: *u8, n: i64) -> i64 {
60 var i: i64 = 0
61 while i >= 0 {
62 let p: i64 = a11_find(buf, i, n, "<img" as *u8)
63 if p < 0 { return 1 }
64 let te: i64 = a11_tag_end(buf, p, n)
65 if a11_attr_nonempty(buf, p, te, "alt=" as *u8) == 0 { return 0 }
66 i = te + 1
67 }
68 return 1
69}
70
71// R4 WCAG 4.1.2: every non-hidden <input> has aria-label=. returns 1 if all OK, 0 otherwise.
72func a11y_inputs_labeled(buf: *u8, n: i64) -> i64 {
73 var i: i64 = 0
74 while i >= 0 {
75 let p: i64 = a11_find(buf, i, n, "<input" as *u8)
76 if p < 0 { return 1 }
77 let te: i64 = a11_tag_end(buf, p, n)
78 var hidden: i64 = 0
79 if a11_find(buf, p, te, "type=\"hidden\"" as *u8) >= 0 { hidden = 1 }
80 if hidden == 0 { if a11_attr_nonempty(buf, p, te, "aria-label=" as *u8) == 0 { return 0 } }
81 i = te + 1
82 }
83 return 1
84}
85
86// R3 WCAG 1.3.1: heading levels not skipped (a jump of >1 going deeper = fail).
87func a11y_heading_order(buf: *u8, n: i64) -> i64 {
88 var prev: i64 = 0
89 var ok: i64 = 1
90 var i: i64 = 0
91 while i + 2 < n {
92 if (buf[i] as i64) == 60 { if (buf[i+1] as i64) == 104 { // "<h"
93 let d: i64 = buf[i+2] as i64
94 if d >= 49 { if d <= 54 { // '1'..'6'
95 let lvl: i64 = d - 48
96 if prev > 0 { if lvl > prev + 1 { ok = 0 } }
97 prev = lvl
98 } }
99 } }
100 i = i + 1
101 }
102 return ok
103}
104
105// full audit: number of the 4 rules satisfied (0..4)
106func a11y_audit(buf: *u8, n: i64) -> i64 {
107 var c: i64 = 0
108 if a11y_has_lang(buf, n) == 1 { c = c + 1 }
109 if a11y_imgs_alt(buf, n) == 1 { c = c + 1 }
110 if a11y_heading_order(buf, n) == 1 { c = c + 1 }
111 if a11y_inputs_labeled(buf, n) == 1 { c = c + 1 }
112 return c
113}