@webround/editor 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks.d.ts +90 -0
- package/dist/blocks.js +119 -0
- package/dist/bridge.d.ts +96 -0
- package/dist/bridge.js +970 -0
- package/dist/check/aria.d.ts +19 -0
- package/dist/check/aria.js +106 -0
- package/dist/check/cli.d.ts +3 -0
- package/dist/check/cli.js +30 -0
- package/dist/check/css.d.ts +13 -0
- package/dist/check/css.js +63 -0
- package/dist/check/html.d.ts +8 -0
- package/dist/check/html.js +29 -0
- package/dist/check/index.d.ts +4 -0
- package/dist/check/index.js +5 -0
- package/dist/check/links.d.ts +7 -0
- package/dist/check/links.js +23 -0
- package/dist/check/orphans.d.ts +20 -0
- package/dist/check/orphans.js +57 -0
- package/dist/check/producer-code.d.ts +53 -0
- package/dist/check/producer-code.js +379 -0
- package/dist/check/producer-dom.d.ts +21 -0
- package/dist/check/producer-dom.js +52 -0
- package/dist/check/project.d.ts +20 -0
- package/dist/check/project.js +51 -0
- package/dist/check/rules.d.ts +7 -0
- package/dist/check/rules.js +517 -0
- package/dist/check/types.d.ts +160 -0
- package/dist/check/types.js +22 -0
- package/dist/components/WrAltcha.d.ts +5 -0
- package/dist/components/WrAltcha.js +8 -0
- package/dist/components/WrButton.d.ts +8 -0
- package/dist/components/WrButton.js +34 -0
- package/dist/components/WrForm.d.ts +6 -0
- package/dist/components/WrForm.js +29 -0
- package/dist/components/WrFormPrivacy.d.ts +4 -0
- package/dist/components/WrFormPrivacy.js +7 -0
- package/dist/components/WrFormSuccess.d.ts +4 -0
- package/dist/components/WrFormSuccess.js +12 -0
- package/dist/components/WrIframe.d.ts +12 -0
- package/dist/components/WrIframe.js +30 -0
- package/dist/components/WrImage.d.ts +9 -0
- package/dist/components/WrImage.js +28 -0
- package/dist/components/WrInput.d.ts +8 -0
- package/dist/components/WrInput.js +29 -0
- package/dist/components/WrLink.d.ts +10 -0
- package/dist/components/WrLink.js +38 -0
- package/dist/components/WrNav.d.ts +26 -0
- package/dist/components/WrNav.js +30 -0
- package/dist/components/WrSection.d.ts +11 -0
- package/dist/components/WrSection.js +5 -0
- package/dist/components/WrShared.d.ts +12 -0
- package/dist/components/WrShared.js +20 -0
- package/dist/components/WrText.d.ts +11 -0
- package/dist/components/WrText.js +41 -0
- package/dist/components/WrVideo.d.ts +9 -0
- package/dist/components/WrVideo.js +73 -0
- package/dist/components/text-editor/CodeThemeContext.d.ts +8 -0
- package/dist/components/text-editor/CodeThemeContext.js +10 -0
- package/dist/components/text-editor/ExampleTheme.d.ts +77 -0
- package/dist/components/text-editor/ExampleTheme.js +76 -0
- package/dist/components/text-editor/ToolbarContext.d.ts +17 -0
- package/dist/components/text-editor/ToolbarContext.js +32 -0
- package/dist/components/text-editor/WrTextEditor.d.ts +13 -0
- package/dist/components/text-editor/WrTextEditor.js +48 -0
- package/dist/components/text-editor/common/constants.d.ts +109 -0
- package/dist/components/text-editor/common/constants.js +58 -0
- package/dist/components/text-editor/common/surface.d.ts +3 -0
- package/dist/components/text-editor/common/surface.js +8 -0
- package/dist/components/text-editor/common/utils.d.ts +7 -0
- package/dist/components/text-editor/common/utils.js +78 -0
- package/dist/components/text-editor/hooks/useLexicalCommitOnBlur.d.ts +5 -0
- package/dist/components/text-editor/hooks/useLexicalCommitOnBlur.js +41 -0
- package/dist/components/text-editor/hooks/useLexicalMarkdown.d.ts +33 -0
- package/dist/components/text-editor/hooks/useLexicalMarkdown.js +284 -0
- package/dist/components/text-editor/nodes/CodeBlockNode.d.ts +23 -0
- package/dist/components/text-editor/nodes/CodeBlockNode.js +138 -0
- package/dist/components/text-editor/nodes/HorizontalRuleNode.d.ts +13 -0
- package/dist/components/text-editor/nodes/HorizontalRuleNode.js +22 -0
- package/dist/components/text-editor/plugins/CodeBlockPlugin.d.ts +2 -0
- package/dist/components/text-editor/plugins/CodeBlockPlugin.js +38 -0
- package/dist/components/text-editor/plugins/FontPlugin.d.ts +12 -0
- package/dist/components/text-editor/plugins/FontPlugin.js +114 -0
- package/dist/components/text-editor/plugins/FontSizePlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/FontSizePlugin.js +105 -0
- package/dist/components/text-editor/plugins/FontWeightPlugin.d.ts +6 -0
- package/dist/components/text-editor/plugins/FontWeightPlugin.js +103 -0
- package/dist/components/text-editor/plugins/HTMLTagPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/HTMLTagPlugin.js +82 -0
- package/dist/components/text-editor/plugins/HorizontalRulePlugin.d.ts +2 -0
- package/dist/components/text-editor/plugins/HorizontalRulePlugin.js +35 -0
- package/dist/components/text-editor/plugins/LineSpacingPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/LineSpacingPlugin.js +83 -0
- package/dist/components/text-editor/plugins/LinkPlugin.d.ts +8 -0
- package/dist/components/text-editor/plugins/LinkPlugin.js +183 -0
- package/dist/components/text-editor/plugins/ListToolbarPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/ListToolbarPlugin.js +40 -0
- package/dist/components/text-editor/plugins/LoadContentPlugin.d.ts +4 -0
- package/dist/components/text-editor/plugins/LoadContentPlugin.js +63 -0
- package/dist/components/text-editor/plugins/MarkdownExportPlugin.d.ts +4 -0
- package/dist/components/text-editor/plugins/MarkdownExportPlugin.js +17 -0
- package/dist/components/text-editor/plugins/MarkdownPastePlugin.d.ts +2 -0
- package/dist/components/text-editor/plugins/MarkdownPastePlugin.js +45 -0
- package/dist/components/text-editor/plugins/SanitizePastePlugin.d.ts +3 -0
- package/dist/components/text-editor/plugins/SanitizePastePlugin.js +40 -0
- package/dist/components/text-editor/plugins/TextAlignPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/TextAlignPlugin.js +65 -0
- package/dist/components/text-editor/plugins/TextColorPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/TextColorPlugin.js +117 -0
- package/dist/components/text-editor/plugins/ToolbarPlugin.d.ts +10 -0
- package/dist/components/text-editor/plugins/ToolbarPlugin.js +73 -0
- package/dist/components/text-editor/theme-colors.d.ts +11 -0
- package/dist/components/text-editor/theme-colors.js +28 -0
- package/dist/context/WrAppContext.d.ts +35 -0
- package/dist/context/WrAppContext.js +113 -0
- package/dist/context/WrLayout.d.ts +15 -0
- package/dist/context/WrLayout.js +22 -0
- package/dist/context/WrPageContext.d.ts +28 -0
- package/dist/context/WrPageContext.js +282 -0
- package/dist/context/restoreValues.d.ts +2 -0
- package/dist/context/restoreValues.js +6 -0
- package/dist/contrast.d.ts +45 -0
- package/dist/contrast.js +122 -0
- package/dist/drop.d.ts +7 -0
- package/dist/drop.js +51 -0
- package/dist/gaps.d.ts +14 -0
- package/dist/gaps.js +50 -0
- package/dist/hooks/useWrMessage.d.ts +5 -0
- package/dist/hooks/useWrMessage.js +24 -0
- package/dist/hooks/useWrSave.d.ts +2 -0
- package/dist/hooks/useWrSave.js +11 -0
- package/dist/index.css +467 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/interactive.d.ts +8 -0
- package/dist/interactive.js +13 -0
- package/dist/measure-rules.d.ts +34 -0
- package/dist/measure-rules.js +54 -0
- package/dist/measure.d.ts +11 -0
- package/dist/measure.js +139 -0
- package/dist/messages.d.ts +860 -0
- package/dist/messages.js +2 -0
- package/dist/nav.d.ts +5 -0
- package/dist/nav.js +10 -0
- package/dist/operations/animations-store.d.ts +6 -0
- package/dist/operations/animations-store.js +63 -0
- package/dist/operations/cli.d.ts +3 -0
- package/dist/operations/cli.js +52 -0
- package/dist/operations/comments-store.d.ts +17 -0
- package/dist/operations/comments-store.js +62 -0
- package/dist/operations/context.d.ts +12 -0
- package/dist/operations/context.js +35 -0
- package/dist/operations/define.d.ts +81 -0
- package/dist/operations/define.js +86 -0
- package/dist/operations/index.d.ts +6 -0
- package/dist/operations/index.js +6 -0
- package/dist/operations/ops/check.d.ts +5 -0
- package/dist/operations/ops/check.js +53 -0
- package/dist/operations/ops/comments.d.ts +6 -0
- package/dist/operations/ops/comments.js +133 -0
- package/dist/operations/ops/content.d.ts +16 -0
- package/dist/operations/ops/content.js +73 -0
- package/dist/operations/ops/json.d.ts +4 -0
- package/dist/operations/ops/json.js +27 -0
- package/dist/operations/ops/langs.d.ts +4 -0
- package/dist/operations/ops/langs.js +32 -0
- package/dist/operations/ops/nodes.d.ts +8 -0
- package/dist/operations/ops/nodes.js +270 -0
- package/dist/operations/ops/pages.d.ts +8 -0
- package/dist/operations/ops/pages.js +154 -0
- package/dist/operations/ops/project.d.ts +36 -0
- package/dist/operations/ops/project.js +97 -0
- package/dist/operations/ops/seo.d.ts +17 -0
- package/dist/operations/ops/seo.js +358 -0
- package/dist/operations/ops/style.d.ts +76 -0
- package/dist/operations/ops/style.js +268 -0
- package/dist/operations/ops/stylesets.d.ts +9 -0
- package/dist/operations/ops/stylesets.js +145 -0
- package/dist/operations/ops/theme.d.ts +6 -0
- package/dist/operations/ops/theme.js +304 -0
- package/dist/operations/registry.d.ts +8 -0
- package/dist/operations/registry.js +19 -0
- package/dist/operations/stylesets-store.d.ts +50 -0
- package/dist/operations/stylesets-store.js +216 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +24 -0
- package/dist/save.d.ts +65 -0
- package/dist/save.js +86 -0
- package/dist/schema.d.ts +69 -0
- package/dist/schema.js +110 -0
- package/dist/scope.d.ts +41 -0
- package/dist/scope.js +44 -0
- package/dist/vite-plugins/ast.d.ts +3 -0
- package/dist/vite-plugins/ast.js +238 -0
- package/dist/vite-plugins/core-swap.d.ts +3 -0
- package/dist/vite-plugins/core-swap.js +57 -0
- package/dist/vite-plugins/create-page.d.ts +4 -0
- package/dist/vite-plugins/create-page.js +59 -0
- package/dist/vite-plugins/guard.d.ts +15 -0
- package/dist/vite-plugins/guard.js +35 -0
- package/dist/vite-plugins/head.d.ts +13 -0
- package/dist/vite-plugins/head.js +53 -0
- package/dist/vite-plugins/index.d.ts +7 -0
- package/dist/vite-plugins/index.js +7 -0
- package/dist/vite-plugins/jsx-tree.d.ts +118 -0
- package/dist/vite-plugins/jsx-tree.js +686 -0
- package/dist/vite-plugins/langs.d.ts +3 -0
- package/dist/vite-plugins/langs.js +44 -0
- package/dist/vite-plugins/operations.d.ts +10 -0
- package/dist/vite-plugins/operations.js +66 -0
- package/dist/vite-plugins/pages-api.d.ts +3 -0
- package/dist/vite-plugins/pages-api.js +112 -0
- package/dist/vite-plugins/pages.d.ts +3 -0
- package/dist/vite-plugins/pages.js +64 -0
- package/dist/vite-plugins/read.d.ts +3 -0
- package/dist/vite-plugins/read.js +45 -0
- package/dist/vite-plugins/seo-files.d.ts +8 -0
- package/dist/vite-plugins/seo-files.js +42 -0
- package/dist/vite-plugins/theme.d.ts +3 -0
- package/dist/vite-plugins/theme.js +207 -0
- package/dist/vite-plugins/update.d.ts +3 -0
- package/dist/vite-plugins/update.js +46 -0
- package/dist/vite-plugins/wr-plugins.d.ts +15 -0
- package/dist/vite-plugins/wr-plugins.js +34 -0
- package/dist/vite-plugins.d.ts +11 -0
- package/dist/vite-plugins.js +518 -0
- package/package.json +96 -5
- package/README.md +0 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface Box {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
right: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
}
|
|
7
|
+
/** WCAG 2.5.8 (AA): a pointer target is at least this many CSS pixels on each side. */
|
|
8
|
+
export declare const MIN_TARGET_PX = 24;
|
|
9
|
+
/**
|
|
10
|
+
* Which of these targets are too small (WCAG 2.5.8): a target under 24×24px passes when a circle of 24px centred on it touches no other target
|
|
11
|
+
* (there is room around it), or when it is a link inside a sentence (a link in a text cannot be larger than its words).
|
|
12
|
+
*/
|
|
13
|
+
export declare function undersizedTargets(targets: readonly {
|
|
14
|
+
box: Box;
|
|
15
|
+
inline: boolean;
|
|
16
|
+
}[]): number[];
|
|
17
|
+
/** Content is lost when a box that clips has MORE inside than it shows after the spacing was spread, and did not before. */
|
|
18
|
+
export declare function lostContent(before: {
|
|
19
|
+
scrollHeight: number;
|
|
20
|
+
clientHeight: number;
|
|
21
|
+
scrollWidth: number;
|
|
22
|
+
clientWidth: number;
|
|
23
|
+
}, after: typeof before): boolean;
|
|
24
|
+
/** Text under this size is hard to read for many people. There is no number in WCAG: this is a rule of thumb, and says so. */
|
|
25
|
+
export declare const MIN_TEXT_PX = 12;
|
|
26
|
+
/** The page is a phone's width or narrower: WCAG 1.4.10 is judged at 320px, so a wider window says nothing. */
|
|
27
|
+
export declare const REFLOW_MAX_WIDTH = 420;
|
|
28
|
+
/**
|
|
29
|
+
* Which navigation areas need a name: when a page has more than one, a screen reader lists them all as "navigation", so each one needs a
|
|
30
|
+
* name of its own (`aria-label`), and not the same as another's. With one, no name is needed. `names` are the names as they are, in the order
|
|
31
|
+
* of the page ('' for none).
|
|
32
|
+
*/
|
|
33
|
+
export declare function navsNeedingAName(names: readonly string[]): number[];
|
|
34
|
+
//# sourceMappingURL=measure-rules.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The decisions of the page's measures, without the page: sizes and boxes in, a verdict out. Pure, so they can be tested without a browser.
|
|
2
|
+
/** WCAG 2.5.8 (AA): a pointer target is at least this many CSS pixels on each side. */
|
|
3
|
+
export const MIN_TARGET_PX = 24;
|
|
4
|
+
/**
|
|
5
|
+
* Which of these targets are too small (WCAG 2.5.8): a target under 24×24px passes when a circle of 24px centred on it touches no other target
|
|
6
|
+
* (there is room around it), or when it is a link inside a sentence (a link in a text cannot be larger than its words).
|
|
7
|
+
*/
|
|
8
|
+
export function undersizedTargets(targets) {
|
|
9
|
+
const radius = MIN_TARGET_PX / 2;
|
|
10
|
+
const small = (box) => box.right - box.left < MIN_TARGET_PX || box.bottom - box.top < MIN_TARGET_PX;
|
|
11
|
+
const centre = (box) => ({ x: (box.left + box.right) / 2, y: (box.top + box.bottom) / 2 });
|
|
12
|
+
// The circle of a small target touches a large one when the large one's box comes within the radius; it touches another small one when
|
|
13
|
+
// their circles overlap (the centres are closer than the diameter)
|
|
14
|
+
const crowds = (box, other) => {
|
|
15
|
+
const { x, y } = centre(box);
|
|
16
|
+
if (small(other)) {
|
|
17
|
+
const o = centre(other);
|
|
18
|
+
return (x - o.x) ** 2 + (y - o.y) ** 2 < MIN_TARGET_PX ** 2;
|
|
19
|
+
}
|
|
20
|
+
const nearestX = Math.max(other.left, Math.min(x, other.right));
|
|
21
|
+
const nearestY = Math.max(other.top, Math.min(y, other.bottom));
|
|
22
|
+
return (x - nearestX) ** 2 + (y - nearestY) ** 2 < radius ** 2;
|
|
23
|
+
};
|
|
24
|
+
const found = [];
|
|
25
|
+
targets.forEach((target, index) => {
|
|
26
|
+
if (target.inline || !small(target.box))
|
|
27
|
+
return;
|
|
28
|
+
if (targets.some((other, i) => i !== index && crowds(target.box, other.box)))
|
|
29
|
+
found.push(index);
|
|
30
|
+
});
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
/** Content is lost when a box that clips has MORE inside than it shows after the spacing was spread, and did not before. */
|
|
34
|
+
export function lostContent(before, after) {
|
|
35
|
+
const over = (a, b) => a > b + 1;
|
|
36
|
+
return (over(after.scrollHeight, after.clientHeight) && !over(before.scrollHeight, before.clientHeight))
|
|
37
|
+
|| (over(after.scrollWidth, after.clientWidth) && !over(before.scrollWidth, before.clientWidth));
|
|
38
|
+
}
|
|
39
|
+
/** Text under this size is hard to read for many people. There is no number in WCAG: this is a rule of thumb, and says so. */
|
|
40
|
+
export const MIN_TEXT_PX = 12;
|
|
41
|
+
/** The page is a phone's width or narrower: WCAG 1.4.10 is judged at 320px, so a wider window says nothing. */
|
|
42
|
+
export const REFLOW_MAX_WIDTH = 420;
|
|
43
|
+
/**
|
|
44
|
+
* Which navigation areas need a name: when a page has more than one, a screen reader lists them all as "navigation", so each one needs a
|
|
45
|
+
* name of its own (`aria-label`), and not the same as another's. With one, no name is needed. `names` are the names as they are, in the order
|
|
46
|
+
* of the page ('' for none).
|
|
47
|
+
*/
|
|
48
|
+
export function navsNeedingAName(names) {
|
|
49
|
+
if (names.length < 2)
|
|
50
|
+
return [];
|
|
51
|
+
const trimmed = names.map(name => name.trim().toLowerCase());
|
|
52
|
+
return trimmed.flatMap((name, index) => (name === '' || trimmed.indexOf(name) !== trimmed.lastIndexOf(name) ? [index] : []));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=measure-rules.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Rgba } from './contrast';
|
|
2
|
+
export interface MeasureFinding {
|
|
3
|
+
/** The rule it belongs to (`color-contrast`, `target-size`…). */
|
|
4
|
+
rule: string;
|
|
5
|
+
/** `data-wr-id` of the nearest element that can be edited. */
|
|
6
|
+
elementId: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
/** Everything the page's drawing says about its accessibility, now. */
|
|
10
|
+
export declare function measurePage(doc: Document, resolve?: (css: string) => Rgba | null): MeasureFinding[];
|
|
11
|
+
//# sourceMappingURL=measure.d.ts.map
|
package/dist/measure.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { canvasResolver, measureContrast } from './contrast';
|
|
2
|
+
import { MIN_TARGET_PX, MIN_TEXT_PX, REFLOW_MAX_WIDTH, lostContent, navsNeedingAName, undersizedTargets } from './measure-rules';
|
|
3
|
+
const idOf = (el) => el.closest('[data-wr-id]')?.getAttribute('data-wr-id') ?? undefined;
|
|
4
|
+
const visible = (el, view) => {
|
|
5
|
+
const style = view.getComputedStyle(el);
|
|
6
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && el.getClientRects().length > 0;
|
|
7
|
+
};
|
|
8
|
+
const ownText = (el) => Array.from(el.childNodes).filter(n => n.nodeType === 3).map(n => n.textContent ?? '').join(' ').replace(/\s+/g, ' ').trim();
|
|
9
|
+
const box = (el) => { const r = el.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom }; };
|
|
10
|
+
const shortText = (text) => (text.length > 40 ? `${text.slice(0, 39)}…` : text);
|
|
11
|
+
const TARGETS = 'a[href], button, input:not([type=hidden]), select, textarea, summary, [role=button], [role=link], [role=checkbox], [role=switch], [role=tab], [role=menuitem], [tabindex]:not([tabindex="-1"])';
|
|
12
|
+
const NAME = { a: 'link', button: 'button', input: 'field', select: 'menu', textarea: 'field', summary: 'summary' };
|
|
13
|
+
function measureTargets(doc) {
|
|
14
|
+
const view = doc.defaultView;
|
|
15
|
+
if (!view)
|
|
16
|
+
return [];
|
|
17
|
+
const elements = Array.from(doc.querySelectorAll(TARGETS)).filter(el => visible(el, view));
|
|
18
|
+
const targets = elements.map(el => {
|
|
19
|
+
const parent = el.parentElement;
|
|
20
|
+
// A link in the middle of a sentence is as large as its words: it is not asked to be 24px
|
|
21
|
+
const inline = el.tagName === 'A' && view.getComputedStyle(el).display === 'inline' && !!parent && (parent.textContent ?? '').trim().length > (el.textContent ?? '').trim().length + 3;
|
|
22
|
+
return { el, box: box(el), inline };
|
|
23
|
+
});
|
|
24
|
+
return undersizedTargets(targets).flatMap((index) => {
|
|
25
|
+
const { el, box: b } = targets[index];
|
|
26
|
+
const elementId = idOf(el);
|
|
27
|
+
if (!elementId)
|
|
28
|
+
return [];
|
|
29
|
+
const label = NAME[el.tagName.toLowerCase()] ?? 'control';
|
|
30
|
+
return [{ rule: 'target-size', elementId, message: `The ${label} is ${Math.round(b.right - b.left)}×${Math.round(b.bottom - b.top)}px: something to press should be at least ${MIN_TARGET_PX}×${MIN_TARGET_PX}px, or have that much free space around it (WCAG 2.5.8).` }];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function measureTextSize(doc) {
|
|
34
|
+
const view = doc.defaultView;
|
|
35
|
+
if (!view || !doc.body)
|
|
36
|
+
return [];
|
|
37
|
+
const smallest = new Map();
|
|
38
|
+
for (const el of Array.from(doc.body.querySelectorAll('*')).slice(0, 4000)) {
|
|
39
|
+
const text = ownText(el);
|
|
40
|
+
if (text.length < 3)
|
|
41
|
+
continue;
|
|
42
|
+
const id = idOf(el);
|
|
43
|
+
if (!id || !visible(el, view))
|
|
44
|
+
continue;
|
|
45
|
+
const px = parseFloat(view.getComputedStyle(el).fontSize);
|
|
46
|
+
if (px >= MIN_TEXT_PX || !Number.isFinite(px))
|
|
47
|
+
continue;
|
|
48
|
+
const known = smallest.get(id);
|
|
49
|
+
if (!known || px < known.px)
|
|
50
|
+
smallest.set(id, { px, text });
|
|
51
|
+
}
|
|
52
|
+
return [...smallest].map(([elementId, { px, text }]) => ({
|
|
53
|
+
rule: 'text-size', elementId,
|
|
54
|
+
message: `The text “${shortText(text)}” is ${Math.round(px * 10) / 10}px high: most people find text under ${MIN_TEXT_PX}px hard to read (WCAG has no minimum, this is a rule of thumb).`,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/** The spacing WCAG 1.4.12 says a page must survive: lines 1.5×, letters 0.12em, words 0.16em apart, paragraphs 2em apart. */
|
|
58
|
+
const SPACING_CSS = 'html *, html *::before, html *::after { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; } html p { margin-bottom: 2em !important; }';
|
|
59
|
+
function measureTextSpacing(doc) {
|
|
60
|
+
const view = doc.defaultView;
|
|
61
|
+
if (!view || !doc.body || !doc.head)
|
|
62
|
+
return [];
|
|
63
|
+
const clippers = Array.from(doc.body.querySelectorAll('*')).slice(0, 4000).filter(el => {
|
|
64
|
+
const style = view.getComputedStyle(el);
|
|
65
|
+
const clips = /hidden|clip/.test(style.overflowX) || /hidden|clip/.test(style.overflowY);
|
|
66
|
+
return clips && el.clientHeight > 0 && ownText(el).length + (el.textContent ?? '').trim().length > 0 && visible(el, view);
|
|
67
|
+
});
|
|
68
|
+
if (clippers.length === 0)
|
|
69
|
+
return [];
|
|
70
|
+
const sizes = (el) => ({ scrollHeight: el.scrollHeight, clientHeight: el.clientHeight, scrollWidth: el.scrollWidth, clientWidth: el.clientWidth });
|
|
71
|
+
const before = clippers.map(sizes);
|
|
72
|
+
// Spread the text, look, put it back: nothing is drawn in between (it all happens in one go of the script)
|
|
73
|
+
const style = doc.createElement('style');
|
|
74
|
+
style.textContent = SPACING_CSS;
|
|
75
|
+
doc.head.appendChild(style);
|
|
76
|
+
let after;
|
|
77
|
+
try {
|
|
78
|
+
after = clippers.map(sizes);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
style.remove();
|
|
82
|
+
}
|
|
83
|
+
const found = new Map();
|
|
84
|
+
clippers.forEach((el, i) => {
|
|
85
|
+
if (!lostContent(before[i], after[i]))
|
|
86
|
+
return;
|
|
87
|
+
const elementId = idOf(el) ?? el.querySelector('[data-wr-id]')?.getAttribute('data-wr-id');
|
|
88
|
+
if (elementId && !found.has(elementId))
|
|
89
|
+
found.set(elementId, { rule: 'text-spacing', elementId, message: 'Text is cut off when its spacing is increased (lines 1.5×, letters and words a little further apart): the box it is in is too tight for it (WCAG 1.4.12).' });
|
|
90
|
+
});
|
|
91
|
+
return [...found.values()];
|
|
92
|
+
}
|
|
93
|
+
function measureReflow(doc) {
|
|
94
|
+
const root = doc.documentElement;
|
|
95
|
+
const width = root.clientWidth;
|
|
96
|
+
if (width === 0 || width > REFLOW_MAX_WIDTH || root.scrollWidth <= width + 1 || !doc.body)
|
|
97
|
+
return [];
|
|
98
|
+
// The element that goes furthest out of the window is where to look
|
|
99
|
+
let culprit;
|
|
100
|
+
for (const el of Array.from(doc.body.querySelectorAll('[data-wr-id]'))) {
|
|
101
|
+
const right = el.getBoundingClientRect().right;
|
|
102
|
+
if (right > width + 1 && (!culprit || right > culprit.right))
|
|
103
|
+
culprit = { id: el.getAttribute('data-wr-id'), right };
|
|
104
|
+
}
|
|
105
|
+
return [{ rule: 'reflow', elementId: culprit?.id ?? '', message: `The page scrolls sideways at ${width}px wide: on a narrow screen the content should fit in one column, without scrolling in two directions (WCAG 1.4.10).` }];
|
|
106
|
+
}
|
|
107
|
+
/** The navigation areas of the page — `<nav>`, `role=navigation` — and whether they can be told apart. They are often in a component's own file: only the drawn page knows them all. */
|
|
108
|
+
function measureNavigation(doc) {
|
|
109
|
+
const view = doc.defaultView;
|
|
110
|
+
if (!view)
|
|
111
|
+
return [];
|
|
112
|
+
const navs = Array.from(doc.querySelectorAll('nav, [role=navigation]')).filter(el => visible(el, view));
|
|
113
|
+
const nameOf = (el) => {
|
|
114
|
+
const label = el.getAttribute('aria-label');
|
|
115
|
+
if (label?.trim())
|
|
116
|
+
return label;
|
|
117
|
+
const by = el.getAttribute('aria-labelledby');
|
|
118
|
+
return by ? by.split(/\s+/).map(id => doc.getElementById(id)?.textContent ?? '').join(' ') : '';
|
|
119
|
+
};
|
|
120
|
+
return navsNeedingAName(navs.map(nameOf)).flatMap((index) => {
|
|
121
|
+
const elementId = idOf(navs[index]);
|
|
122
|
+
const name = nameOf(navs[index]).trim();
|
|
123
|
+
return [{
|
|
124
|
+
rule: 'landmark-nav-name', elementId: elementId ?? '',
|
|
125
|
+
message: name
|
|
126
|
+
? `There are ${navs.length} navigation areas and two have the name "${name}": a screen reader lists them as ${navs.length} identical entries. Give each its own aria-label.`
|
|
127
|
+
: `There are ${navs.length} navigation areas and this one has no name: a screen reader lists them all as "navigation". Give each its own aria-label (Main, Footer…).`,
|
|
128
|
+
}];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Everything the page's drawing says about its accessibility, now. */
|
|
132
|
+
export function measurePage(doc, resolve = canvasResolver(doc)) {
|
|
133
|
+
const contrast = measureContrast(doc, resolve).map((f) => ({
|
|
134
|
+
rule: 'color-contrast', elementId: f.elementId,
|
|
135
|
+
message: `The text “${f.text}” has a contrast of ${f.ratio}:1 against its background; it should be at least ${f.required}:1 (WCAG 1.4.3).`,
|
|
136
|
+
}));
|
|
137
|
+
return [...contrast, ...measureTargets(doc), ...measureTextSize(doc), ...measureTextSpacing(doc), ...measureReflow(doc), ...measureNavigation(doc)];
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=measure.js.map
|