@webround/editor 0.3.12 → 0.3.14
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.
|
@@ -225,7 +225,7 @@ function fromJsx(el, config, file, options) {
|
|
|
225
225
|
const children = el.isKind(SyntaxKind.JsxElement) ? nodesUnder(el, config, file, options) : [];
|
|
226
226
|
// Plain components we know nothing about (`<Card>`) are only transparent: what they hold is what counts. A shared one is entered: what it renders is part of the page
|
|
227
227
|
if (!wrKind && /^[A-Z]/.test(name))
|
|
228
|
-
return [...sharedNodesOf(el, name, options), ...children];
|
|
228
|
+
return [...sharedNodesOf(el, name, options), ...children].map(node => ({ ...node, viaComponent: true }));
|
|
229
229
|
const attrs = {};
|
|
230
230
|
const alt = inLang(json.alt, options) ?? str(attrOf(el, 'alt'));
|
|
231
231
|
if (alt !== undefined)
|
|
@@ -6,6 +6,8 @@ export interface DomNode {
|
|
|
6
6
|
tag: string;
|
|
7
7
|
attrs: Record<string, string>;
|
|
8
8
|
text: string;
|
|
9
|
+
/** The element's own `id` (what `aria-labelledby` points at), not its `data-wr-id`. */
|
|
10
|
+
htmlId?: string;
|
|
9
11
|
/** A form field that has a `<label>`: it sits inside one, or one points at it (`for`). */
|
|
10
12
|
labelled?: true;
|
|
11
13
|
children: DomNode[];
|
|
@@ -23,6 +23,7 @@ export function serializeDom(root) {
|
|
|
23
23
|
const labelled = fields.includes(tag) && (el.closest?.('label') != null || (fieldId !== null && pointedAt.has(fieldId)));
|
|
24
24
|
return {
|
|
25
25
|
...(id ? { id } : {}),
|
|
26
|
+
...(fieldId ? { htmlId: fieldId } : {}),
|
|
26
27
|
wrType: el.getAttribute('data-wr-type'),
|
|
27
28
|
tag,
|
|
28
29
|
attrs,
|
|
@@ -44,7 +45,11 @@ function toCheckNode(node) {
|
|
|
44
45
|
}
|
|
45
46
|
/** The tree a rule sees, from what `serializeDom` read (wherever it ran). */
|
|
46
47
|
export function checkPageFromSerialized(name, nodes) {
|
|
47
|
-
|
|
48
|
+
const elementIds = [];
|
|
49
|
+
const collect = (list) => list.forEach(node => { if (node.htmlId)
|
|
50
|
+
elementIds.push(node.htmlId); collect(node.children); });
|
|
51
|
+
collect(nodes);
|
|
52
|
+
return { name, source: 'dom', nodes: nodes.map(toCheckNode), elementIds };
|
|
48
53
|
}
|
|
49
54
|
export function checkPageFromDom(name, root) {
|
|
50
55
|
return checkPageFromSerialized(name, serializeDom(root));
|
package/dist/check/rules.js
CHANGED
|
@@ -49,6 +49,35 @@ export const RULES = [
|
|
|
49
49
|
report(null, `${problem.message} (${problem.file}:${problem.line})`);
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
name: 'html-structure',
|
|
54
|
+
severity: 'warning',
|
|
55
|
+
run(page, report) {
|
|
56
|
+
// What a tag may hold directly, and what it must sit in: the HTML content model, for the tags a WrSection can be. A node under a component
|
|
57
|
+
// we do not read (`viaComponent`) is not judged: that component may draw the list around it.
|
|
58
|
+
const OWN_CHILDREN = { ul: ['li'], ol: ['li'], menu: ['li'] };
|
|
59
|
+
const MUST_SIT_IN = { li: ['ul', 'ol', 'menu'], dt: ['dl', 'div'], dd: ['dl', 'div'], figcaption: ['figure'], summary: ['details'] };
|
|
60
|
+
const INTERACTIVE = new Set(['a', 'button']);
|
|
61
|
+
const visit = (node, parent, interactive) => {
|
|
62
|
+
if (!node.viaComponent) {
|
|
63
|
+
const allowed = parent ? OWN_CHILDREN[parent.tag] : undefined;
|
|
64
|
+
if (parent && allowed && !allowed.includes(node.tag))
|
|
65
|
+
report(node, `A <${node.tag}> inside a <${parent.tag}>: a list holds only <${allowed.join('>, <')}>.`);
|
|
66
|
+
const sits = MUST_SIT_IN[node.tag];
|
|
67
|
+
if (parent && sits && !sits.includes(parent.tag) && !parent.children.some(c => c.viaComponent))
|
|
68
|
+
report(node, `A <${node.tag}> must be inside ${sits.map(t => `a <${t}>`).join(' or ')}, not a <${parent.tag}>.`);
|
|
69
|
+
}
|
|
70
|
+
// Links and buttons cannot hold each other: a browser repairs it in a way that breaks the page
|
|
71
|
+
if (INTERACTIVE.has(node.tag) && interactive && !node.attrs['{spread}'])
|
|
72
|
+
report(node, `A <${node.tag}> inside a <${interactive.tag}>: an interactive element cannot hold another.`);
|
|
73
|
+
const next = INTERACTIVE.has(node.tag) ? node : interactive;
|
|
74
|
+
for (const child of node.children)
|
|
75
|
+
visit(child, node, next);
|
|
76
|
+
};
|
|
77
|
+
for (const node of page.nodes)
|
|
78
|
+
visit(node, null, null);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
52
81
|
{
|
|
53
82
|
name: 'img-alt',
|
|
54
83
|
severity: 'warning',
|
|
@@ -222,7 +251,8 @@ export const RULES = [
|
|
|
222
251
|
if (n.kind !== 'input')
|
|
223
252
|
continue;
|
|
224
253
|
const written = Object.values(n.values?.label ?? {}).some(text => text.trim() !== '');
|
|
225
|
-
|
|
254
|
+
// A drawn page has no JSON: there the label is the `<label>` the field sits in or the one that points at it (`insideLabel`, as `form-label` reads it)
|
|
255
|
+
if (!written && !n.attrs['aria-label']?.trim() && !n.insideLabel)
|
|
226
256
|
report(n, 'A form field has no label: a placeholder is not one, and the owner reads this name in the email.');
|
|
227
257
|
}
|
|
228
258
|
},
|
|
@@ -304,7 +334,8 @@ export const RULES = [
|
|
|
304
334
|
name: 'aria-reference',
|
|
305
335
|
severity: 'warning',
|
|
306
336
|
run(page, report) {
|
|
307
|
-
|
|
337
|
+
// The Wr elements' ids, and (a drawn page) every `id` of the HTML: a plain element can be pointed at too
|
|
338
|
+
const ids = new Set([...everyNode(page).map(n => n.id).filter((id) => !!id), ...(page.elementIds ?? [])]);
|
|
308
339
|
for (const n of everyNode(page)) {
|
|
309
340
|
for (const attribute of ['aria-labelledby', 'aria-describedby']) {
|
|
310
341
|
const value = n.attrs[attribute];
|
package/dist/check/types.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface CheckNode {
|
|
|
26
26
|
key: string;
|
|
27
27
|
side: string;
|
|
28
28
|
}[];
|
|
29
|
+
/** Only the code producer says it: the node sits under a component we do not read (`<Card>`), so what is around it may be drawn by that component; the nesting rules do not judge it. */
|
|
30
|
+
viaComponent?: true;
|
|
29
31
|
/** Shown as hidden in the JSON: the checks do not count it (it does not render), an outline says so. */
|
|
30
32
|
hidden?: true;
|
|
31
33
|
/** What the element says in each language, for the fields that are translated (`content`, `alt`, `ariaLabel`): field → language key → value. */
|
|
@@ -53,6 +55,8 @@ export interface CheckPage {
|
|
|
53
55
|
project?: CheckProject;
|
|
54
56
|
/** The ids of the form fields some `<label htmlFor>` of the code points at. */
|
|
55
57
|
labelledIds?: string[];
|
|
58
|
+
/** Every `id` the HTML of the page has (only the DOM producer knows them: in the code a plain element's id can be computed). */
|
|
59
|
+
elementIds?: string[];
|
|
56
60
|
/** What the page tells search engines (only the code producer knows it: the DOM has no `config`): text by language key, `default` first. */
|
|
57
61
|
seo?: {
|
|
58
62
|
title: Record<string, string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webround/editor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@lexical/selection": "^0.51.0",
|
|
76
76
|
"@lexical/utils": "^0.51.0",
|
|
77
77
|
"@monaco-editor/react": "^4.7.0",
|
|
78
|
-
"@webround/core": "^0.3.
|
|
78
|
+
"@webround/core": "^0.3.14",
|
|
79
79
|
"highlight.js": "^11.12.0",
|
|
80
80
|
"hls.js": "^1.5.0",
|
|
81
81
|
"lexical": "^0.51.0",
|