@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
- return { name, source: 'dom', nodes: nodes.map(toCheckNode) };
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));
@@ -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
- if (!written && !n.attrs['aria-label']?.trim())
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
- const ids = new Set(everyNode(page).map(n => n.id).filter((id) => !!id));
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];
@@ -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.12",
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.12",
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",