nodality 1.2.10 → 1.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/bin/mcp-server.mjs +131 -0
- package/bin/schema-cli.mjs +55 -11
- package/dist/animator.cjs.js +1 -1
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +1 -28
- package/dist/button.cjs.js +1 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +1 -28
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +1 -28
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +1 -28
- package/dist/flex-card.cjs.js +1 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +1 -28
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +1 -28
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +15 -2
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/dropdown-2025.js +10 -3
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +14 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +5 -3
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +15 -2
- package/layout/index.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +11 -3
- package/layout/side-nav-bar.js +16 -3
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +16 -1
- package/layout/text.js +28 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/agent-surface.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/check-page.js +239 -0
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +64 -2
- package/lib/element-mapper.js +113 -23
- package/lib/element-params.generated.js +2 -2
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +8 -1
- package/lib/normalize-spec.js +134 -0
- package/lib/parse-html.js +14 -2
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +1 -1
- package/lib/raster-presets.js +1 -1
- package/lib/resolve-refs.js +121 -0
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +1 -1
- package/lib/validate-nodes.js +99 -3
- package/lib/webmcp-adapter.js +1 -1
- package/package.json +6 -2
- package/schema.json +8152 -0
- package/scripts/generate-schema.mjs +251 -0
- package/skills/nodality/SKILL.md +121 -1
package/lib/designer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -13,6 +13,8 @@ import { ElementMapper } from "../lib/element-mapper.js";
|
|
|
13
13
|
import { applyMorphNodes } from "../lib/morph-node.js";
|
|
14
14
|
import { installAgentSurface } from "../lib/webmcp-adapter.js";
|
|
15
15
|
import { annotateRoundTrip } from "../lib/parse-html.js";
|
|
16
|
+
import { resolveRefs, collectRefs } from "../lib/resolve-refs.js";
|
|
17
|
+
import { normalizeSpec } from "../lib/normalize-spec.js";
|
|
16
18
|
import { Animator } from "../layout/animator.js";
|
|
17
19
|
import { Base } from "../layout/base.js";
|
|
18
20
|
import { Text } from "../layout/text.js";
|
|
@@ -435,7 +437,33 @@ new Text("row.").set({
|
|
|
435
437
|
// Kept for phase T: a morph node resolves `from`/`to` ids against
|
|
436
438
|
// the rendered children BY POSITION, so it needs the descriptor
|
|
437
439
|
// order — components do not all carry their id into the DOM.
|
|
438
|
-
|
|
440
|
+
// Tier 3: `{ $ref: "name" }` is expanded here, so everything
|
|
441
|
+
// downstream — the mapper, morph nodes resolving by position, the
|
|
442
|
+
// agent surface, annotation — sees ordinary elements and needs no
|
|
443
|
+
// knowledge of references at all. No defs, no change: `resolveRefs`
|
|
444
|
+
// returns its input untouched.
|
|
445
|
+
arr = resolveRefs(Array.isArray(arr) ? arr : [], this._defs);
|
|
446
|
+
|
|
447
|
+
// Tier 6: `src`/`href` mean `url`, `options` means `items`. Rewritten
|
|
448
|
+
// once, here, so the mapper and every component below it only ever see
|
|
449
|
+
// the canonical names and none of them needs to know an alias exists.
|
|
450
|
+
// Additive — a spec using the canonical names comes through unchanged.
|
|
451
|
+
arr = normalizeSpec(arr);
|
|
452
|
+
|
|
453
|
+
// A name with no definition is refused here, naming what is defined.
|
|
454
|
+
// Left alone it reaches the mapper as an element with no `type` and
|
|
455
|
+
// fails as `Unknown element type "undefined"`, which sends the reader
|
|
456
|
+
// looking for a typo in a type rather than a missing definition.
|
|
457
|
+
{
|
|
458
|
+
const dangling = collectRefs(arr).filter(
|
|
459
|
+
(r) => !this._defs || !Object.prototype.hasOwnProperty.call(this._defs, r.name));
|
|
460
|
+
if (dangling.length) {
|
|
461
|
+
const defined = Object.keys(this._defs || {});
|
|
462
|
+
throw new Error(
|
|
463
|
+
`[nodality] no definition for $ref "${dangling[0].name}" at ` +
|
|
464
|
+
`${dangling[0].path}. Defined: ${defined.length ? defined.join(", ") : "(none)"}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
439
467
|
|
|
440
468
|
this.css = [];
|
|
441
469
|
this.elCSS = [];
|
|
@@ -466,6 +494,15 @@ customOptions.forEach(item => {
|
|
|
466
494
|
...arr.filter((e) => !e || (e.type !== "nav" && e.type !== "sideNav")),
|
|
467
495
|
];
|
|
468
496
|
|
|
497
|
+
// Recorded AFTER the lift, not before. Everything that consumes
|
|
498
|
+
// `_elements` matches it against rendered children BY POSITION —
|
|
499
|
+
// morph resolving `from`/`to`, and the round-trip writing each
|
|
500
|
+
// descriptor onto the node it produced. The array was being stored in
|
|
501
|
+
// the author's order while the page rendered in this one, so any spec
|
|
502
|
+
// with a nav after another element mapped descriptors to the wrong
|
|
503
|
+
// nodes. Found while wiring Tier 3, which needed the same array.
|
|
504
|
+
this._elements = arr;
|
|
505
|
+
|
|
469
506
|
for (var i = 0; i < arr.length; i++) {
|
|
470
507
|
let el = arr[i];
|
|
471
508
|
let ela = null;
|
|
@@ -1339,6 +1376,31 @@ new Slider(texts, null, { tintColor: "#e74c3c", inactiveColor: "#ccc" })
|
|
|
1339
1376
|
return this;
|
|
1340
1377
|
}
|
|
1341
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* Shared fragments, referenced from E as `{ $ref: "name" }`.
|
|
1381
|
+
*
|
|
1382
|
+
* Ordinary data: one object, imported by every page's entry, so the
|
|
1383
|
+
* sharing is a plain import rather than a new authoring mode. Worth it
|
|
1384
|
+
* from two pages up — at one page a reference costs more than inlining,
|
|
1385
|
+
* which is why nothing here is automatic.
|
|
1386
|
+
*
|
|
1387
|
+
* MUST be called before `add`. `add` renders as it goes, so definitions
|
|
1388
|
+
* arriving afterwards are simply too late — and the failure was
|
|
1389
|
+
* "Unknown element type undefined" from deep in the mapper, which says
|
|
1390
|
+
* nothing about the real mistake. The wrong order is now refused where it
|
|
1391
|
+
* happens, in the words of the thing the caller did.
|
|
1392
|
+
*/
|
|
1393
|
+
defs(map) {
|
|
1394
|
+
if (this._elements !== undefined) {
|
|
1395
|
+
throw new Error(
|
|
1396
|
+
'[nodality] .defs() must come before .add(): add() renders as it ' +
|
|
1397
|
+
'goes, so definitions given afterwards cannot be used. Chain it as ' +
|
|
1398
|
+
'new Des().defs({ ... }).nodes(N).add(E)');
|
|
1399
|
+
}
|
|
1400
|
+
this._defs = (map && typeof map === "object" && !Array.isArray(map)) ? map : {};
|
|
1401
|
+
return this;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1342
1404
|
set(obj) {
|
|
1343
1405
|
// Emit-only mode: invoked from `nodality compile`. Capture the
|
|
1344
1406
|
// imperative code array (the same content the `code: true` on-page
|
package/lib/element-mapper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -88,6 +88,17 @@ const ELEMENT_TYPES = [
|
|
|
88
88
|
// ignoring it was the defect Stage 4 fixes - throwing on it would be a worse
|
|
89
89
|
// one. Missing fields are filled rather than left undefined, because the
|
|
90
90
|
// mapper derives an id from the title and a missing title threw.
|
|
91
|
+
// One id, however it was spelled. `#hero` and `hero` name the same element,
|
|
92
|
+
// and until now which one worked depended on the subsystem: raster targeting
|
|
93
|
+
// matched by exact string, morph-node stripped the hash, and chainReact
|
|
94
|
+
// required it. Both spellings are accepted everywhere now, and neither is made
|
|
95
|
+
// canonical — normalising only at the point of comparison means every existing
|
|
96
|
+
// page keeps working whichever it used.
|
|
97
|
+
const sameId = (a, b) => {
|
|
98
|
+
const bare = (v) => typeof v === "string" && v.charAt(0) === "#" ? v.slice(1) : v;
|
|
99
|
+
return bare(a) === bare(b);
|
|
100
|
+
};
|
|
101
|
+
|
|
91
102
|
const navEntry = (i) =>
|
|
92
103
|
(typeof i === "string" || typeof i === "number")
|
|
93
104
|
? { title: String(i), link: "#e" }
|
|
@@ -202,8 +213,12 @@ class ElementMapper { // 22:09:58 04/11/2024
|
|
|
202
213
|
font: "Arial"
|
|
203
214
|
},
|
|
204
215
|
headStyle: {
|
|
216
|
+
// White on #ff6d22 measured 2.81:1 against a 4.5:1
|
|
217
|
+
// requirement — a header that reads as styled and is not
|
|
218
|
+
// legible. #c2410c is 5.18:1, and is where the card title
|
|
219
|
+
// moved to, so the palette stays one palette.
|
|
205
220
|
color: "white",
|
|
206
|
-
background: "#
|
|
221
|
+
background: "#c2410c"
|
|
207
222
|
},
|
|
208
223
|
border: "2px solid black",
|
|
209
224
|
center: true,
|
|
@@ -546,6 +561,13 @@ animation: {
|
|
|
546
561
|
return this.attachRaster(new FloatingInput()
|
|
547
562
|
.set({
|
|
548
563
|
type: el.inputType ?? "input",
|
|
564
|
+
// FloatingInput calls it `title`; every generator and every
|
|
565
|
+
// UI kit calls it `label`, and both solvers in the eval
|
|
566
|
+
// wrote `label`. The field it produced then had no
|
|
567
|
+
// accessible name at all — a placeholder is not one, since
|
|
568
|
+
// it disappears the moment anyone types.
|
|
569
|
+
...(el.label !== undefined && el.title === undefined
|
|
570
|
+
? { title: el.label } : {}),
|
|
549
571
|
...this.elOpts(el),
|
|
550
572
|
}), el, obj.customOptions);
|
|
551
573
|
}
|
|
@@ -578,6 +600,13 @@ animation: {
|
|
|
578
600
|
|
|
579
601
|
|
|
580
602
|
static dropdown(obj){
|
|
603
|
+
// A label for the trigger, which is what makes every declared option
|
|
604
|
+
// selectable. Without one the first item becomes the label and is
|
|
605
|
+
// spent — `items: ["CZE","POL","DEU"]` offered two. `label` is the
|
|
606
|
+
// name a generator reaches for (it is what <select> and every UI kit
|
|
607
|
+
// call it); `title` is what the component calls it, so both are taken.
|
|
608
|
+
const ddLabel = obj.el.label ?? obj.el.title;
|
|
609
|
+
|
|
581
610
|
if (!obj.el.items){
|
|
582
611
|
obj.el.items = ["Flower", "Car", "Maseratti"];
|
|
583
612
|
}
|
|
@@ -604,6 +633,9 @@ animation: {
|
|
|
604
633
|
// `raster:` option here would be silently dropped.
|
|
605
634
|
const dd = new Dropdown()
|
|
606
635
|
.set({
|
|
636
|
+
// Supplied only when there is one, so a dropdown without a label
|
|
637
|
+
// keeps the shape it always had, byte for byte.
|
|
638
|
+
...(ddLabel === undefined ? {} : { title: ddLabel }),
|
|
607
639
|
behaviour: "click", // click otherwise
|
|
608
640
|
//width: "120px",
|
|
609
641
|
|
|
@@ -623,7 +655,7 @@ animation: {
|
|
|
623
655
|
new Text("Option 3"),*/
|
|
624
656
|
|
|
625
657
|
|
|
626
|
-
new Text(obj.el.items[0]).set({
|
|
658
|
+
...(ddLabel !== undefined ? [] : [new Text(obj.el.items[0]).set({
|
|
627
659
|
font: "Arial",
|
|
628
660
|
fluidc: "S6",
|
|
629
661
|
weight: "bold",
|
|
@@ -635,10 +667,12 @@ animation: {
|
|
|
635
667
|
},
|
|
636
668
|
pad: [{l: 10, r: 10}],
|
|
637
669
|
|
|
638
|
-
}),
|
|
670
|
+
})]),
|
|
639
671
|
|
|
640
672
|
new Wrapper().set({border: "1px solid green", background: "#1abc9c", radius: "0.7rem", socenter: true})
|
|
641
|
-
|
|
673
|
+
// Every item is an option when the trigger carries its own label;
|
|
674
|
+
// otherwise the first is spent on the label, as before.
|
|
675
|
+
.add(ddLabel === undefined ? items.slice(1) : items
|
|
642
676
|
|
|
643
677
|
/* new Text("Option 1o").set({font: "Arial", pad: [{a: 10}],}),
|
|
644
678
|
new Text("Option 2").set({font: "Arial", pad: [{a: 10}],}),
|
|
@@ -719,8 +753,10 @@ alert("PP")
|
|
|
719
753
|
const navItems = Array.isArray(obj.el?.items) && obj.el.items.length
|
|
720
754
|
? obj.el.items.map(navEntry)
|
|
721
755
|
: null;
|
|
756
|
+
// `pad` gives the link a hit area: unpadded these measured 43x18,
|
|
757
|
+
// under the 24px WCAG 2.2 minimum in the direction a thumb needs.
|
|
722
758
|
const navLinks = () => navItems.map((i) =>
|
|
723
|
-
new Link().set({ text: i.title, url: i.link }));
|
|
759
|
+
new Link().set({ text: i.title, url: i.link, pad: [{ tb: 8, lr: 4 }] }));
|
|
724
760
|
|
|
725
761
|
let links = items.map((i, o) => new Link(`"${i.title}"`)
|
|
726
762
|
.set({
|
|
@@ -1114,6 +1150,12 @@ new Wrapper().set({
|
|
|
1114
1150
|
|
|
1115
1151
|
// Phase S4, as protoNav above: `items` carries {title, link}; absent,
|
|
1116
1152
|
// the placeholders are used exactly as before.
|
|
1153
|
+
// Whether the caller supplied anything, kept separate from `items`
|
|
1154
|
+
// itself: the placeholders carry a `link` of their own that was never
|
|
1155
|
+
// rendered, so honouring it now would change output that has to stay
|
|
1156
|
+
// byte-identical. Supplied entries use their link; placeholders keep
|
|
1157
|
+
// the hardcoded url they have always rendered.
|
|
1158
|
+
const suppliedNav = Array.isArray(obj.el?.items) && obj.el.items.length > 0;
|
|
1117
1159
|
let items = Array.isArray(obj.el?.items) && obj.el.items.length
|
|
1118
1160
|
? obj.el.items.map(navEntry)
|
|
1119
1161
|
: [
|
|
@@ -1139,7 +1181,11 @@ new Wrapper().set({
|
|
|
1139
1181
|
text: i.title,
|
|
1140
1182
|
link: i.link,
|
|
1141
1183
|
isNavA: true,
|
|
1142
|
-
|
|
1184
|
+
// The caller's link, not a hardcoded one. `link:` is not read by
|
|
1185
|
+
// Link — `url` is what becomes the href — so declaring a link here
|
|
1186
|
+
// used to render a fixed value regardless, which is the silent
|
|
1187
|
+
// no-op this whole plan exists to remove.
|
|
1188
|
+
url: suppliedNav ? (i.link || "#e") : "#e",
|
|
1143
1189
|
id: "#" + i.title.toLowerCase(),
|
|
1144
1190
|
font: "Arial",
|
|
1145
1191
|
pad: [{a:20}],
|
|
@@ -1240,7 +1286,11 @@ new Wrapper().set({
|
|
|
1240
1286
|
text: i.title,
|
|
1241
1287
|
link: i.link,
|
|
1242
1288
|
isNavA: true,
|
|
1243
|
-
|
|
1289
|
+
// The caller's link, not a hardcoded one. `link:` is not read by
|
|
1290
|
+
// Link — `url` is what becomes the href — so declaring a link here
|
|
1291
|
+
// used to render a fixed value regardless, which is the silent
|
|
1292
|
+
// no-op this whole plan exists to remove.
|
|
1293
|
+
url: suppliedNav ? (i.link || "#myURL") : "#myURL",
|
|
1244
1294
|
id: "#" + i.title.toLowerCase(),
|
|
1245
1295
|
font: "Arial",
|
|
1246
1296
|
pad: [{a:20}],
|
|
@@ -1347,7 +1397,11 @@ if (obj.el.dropdown){
|
|
|
1347
1397
|
radius: "1rem",
|
|
1348
1398
|
isSide: true,
|
|
1349
1399
|
background: "#ecf0f1",
|
|
1350
|
-
|
|
1400
|
+
// Both hamburger states measured under 3:1 on white at 26px:
|
|
1401
|
+
// #e67e22 was 2.48:1 and #1abc9c 2.16:1. Same hues, dark enough
|
|
1402
|
+
// to be seen — and #c2410c is the orange the cards and the table
|
|
1403
|
+
// header moved to, so one palette rather than three.
|
|
1404
|
+
hamColour: {opened: "#0f766e", closed: "#c2410c"},
|
|
1351
1405
|
mobileSize: "1.2em",
|
|
1352
1406
|
fixed: true,
|
|
1353
1407
|
tags: {open: "sidebar:open", close: "sidebar:closed"}
|
|
@@ -1518,6 +1572,12 @@ let elo = new Polygon({ id: "hex" })
|
|
|
1518
1572
|
class: el.class,
|
|
1519
1573
|
color: el.color,
|
|
1520
1574
|
size: this.getElType(el.type), // update 23/07/2025
|
|
1575
|
+
//@ tag: Heading level to render, independent of the type's size.
|
|
1576
|
+
// Without this the option exists on the component and is not
|
|
1577
|
+
// reachable from a spec, which for a data-first format means it
|
|
1578
|
+
// does not exist. The card template uses it; an author writing
|
|
1579
|
+
// {type:"h5", tag:"h2"} must get the same lever.
|
|
1580
|
+
tag: el.tag,
|
|
1521
1581
|
font: el.font ?? "Arial",
|
|
1522
1582
|
// index: obj.i + "",
|
|
1523
1583
|
// keySet: {key: "border", value: "3px solid green"},
|
|
@@ -1678,15 +1738,12 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1678
1738
|
})
|
|
1679
1739
|
.items([
|
|
1680
1740
|
new Image(item.img).set({isFull: true, url: item.img}),
|
|
1681
|
-
// #
|
|
1682
|
-
|
|
1683
|
-
// this block lives inside a template literal and is evaluated as
|
|
1684
|
-
// source, so module scope is not visible either.
|
|
1685
|
-
new Text(item.title).set({ size: "S5", color: "#f97316" }),
|
|
1741
|
+
// #c2410c is 5.18:1 on white; h2 keeps the outline intact.
|
|
1742
|
+
new Text(item.title).set({ size: "S5", tag: "h2", color: "#c2410c" }),
|
|
1686
1743
|
new Link("Link").set({
|
|
1687
1744
|
text: item.title,
|
|
1688
1745
|
url: item.link,
|
|
1689
|
-
background: "#
|
|
1746
|
+
background: "#1d6fe0",
|
|
1690
1747
|
pad: [{ lr: "0.5rem", tb: "1rem" }],
|
|
1691
1748
|
radius: "0.4rem",
|
|
1692
1749
|
color: "white",
|
|
@@ -1694,6 +1751,28 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1694
1751
|
})
|
|
1695
1752
|
])`;
|
|
1696
1753
|
|
|
1754
|
+
// WHY THE CARD'S COLOURS AND HEADING LEVEL ARE WHAT THEY ARE.
|
|
1755
|
+
//
|
|
1756
|
+
// Kept OUT of the template above on purpose. Everything inside that string
|
|
1757
|
+
// is source the developer receives, so a paragraph of rationale there is a
|
|
1758
|
+
// paragraph in every generated page: fourteen lines of commentary about
|
|
1759
|
+
// contrast ratios added 700 characters to the emitted output before this
|
|
1760
|
+
// was noticed. Moving it here — and taking the older four-line note with
|
|
1761
|
+
// it — left the emission 235 characters SMALLER than before this work.
|
|
1762
|
+
//
|
|
1763
|
+
// The title was #f97316, chosen over the CSS keyword orange because that
|
|
1764
|
+
// reads yellow beside modern palettes. A considered choice that measured
|
|
1765
|
+
// 2.80:1 on white against a 3:1 requirement and missed it by a margin
|
|
1766
|
+
// nobody catches by looking. #c2410c is 5.18:1 and clears both the large-
|
|
1767
|
+
// and small-text thresholds whatever size the title renders at. The link
|
|
1768
|
+
// background moved from #3498db (white on it: 3.15:1) to #1d6fe0 (4.77:1),
|
|
1769
|
+
// already this library's blue elsewhere.
|
|
1770
|
+
//
|
|
1771
|
+
// The tag option decouples the document outline from the type scale: the
|
|
1772
|
+
// title stays S5-sized and stops being an h5, which made every page with
|
|
1773
|
+
// an h1 above a card grid skip h2, h3 and h4 at once. h2, because a card
|
|
1774
|
+
// grid is normally a section of a page rather than a subsection of one;
|
|
1775
|
+
// pass tag to nest it deeper.
|
|
1697
1776
|
// zoom card block (string template)
|
|
1698
1777
|
const zoomCard = `
|
|
1699
1778
|
new ZoomCard()
|
|
@@ -1705,11 +1784,11 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1705
1784
|
useBrightness: true
|
|
1706
1785
|
})
|
|
1707
1786
|
.items([
|
|
1708
|
-
new Text(item.title).set({ fluidc: "S6", color: "#
|
|
1787
|
+
new Text(item.title).set({ fluidc: "S6", tag: "h2", color: "#c2410c" }),
|
|
1709
1788
|
new Link("Link").set({
|
|
1710
1789
|
text: item.title,
|
|
1711
1790
|
url: item.link,
|
|
1712
|
-
background: "#
|
|
1791
|
+
background: "#1d6fe0",
|
|
1713
1792
|
pad: [{ lr: "0.5rem", tb: "1rem" }],
|
|
1714
1793
|
radius: "0.4rem",
|
|
1715
1794
|
color: "white"
|
|
@@ -1806,12 +1885,23 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1806
1885
|
const shorthand = items.filter((it) => !isSpecList(it));
|
|
1807
1886
|
const specLists = items.filter(isSpecList);
|
|
1808
1887
|
|
|
1888
|
+
// Alt text, forwarded only when some entry carries it. The template
|
|
1889
|
+
// is emitted SOURCE, so anything added here is paid by every generated
|
|
1890
|
+
// page — including the no-items path, whose output is pinned byte for
|
|
1891
|
+
// byte. Widening the template only when the data needs it keeps that
|
|
1892
|
+
// pin intact and still lets the terse form describe its images, which
|
|
1893
|
+
// matters because the terse form is the one a generator reaches for.
|
|
1894
|
+
const altTemplate = (tpl) =>
|
|
1895
|
+
shorthand.some((it) => it && it.alt !== undefined)
|
|
1896
|
+
? tpl.replace("url: item.img}", "url: item.img, alt: item.alt}")
|
|
1897
|
+
: tpl;
|
|
1898
|
+
|
|
1809
1899
|
// Every entry is shorthand: keep the `.map()` form, so the emitted
|
|
1810
1900
|
// code stays as compact as the hand-written equivalent and the
|
|
1811
1901
|
// template is still paid once.
|
|
1812
1902
|
if (specLists.length === 0) {
|
|
1813
1903
|
return `${JSON.stringify(shorthand, null, 2).split("\n").map((l, i) => i === 0 ? l : " " + l).join("\n")}.map(item =>
|
|
1814
|
-
${cardTemplate}
|
|
1904
|
+
${altTemplate(cardTemplate)}
|
|
1815
1905
|
)`;
|
|
1816
1906
|
}
|
|
1817
1907
|
|
|
@@ -1852,9 +1942,9 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1852
1942
|
return `new Card()
|
|
1853
1943
|
.set({ width: "300px", height: "700px", radius: "0.7rem", mar: { sides: ["all"], value: "0.8rem" } })
|
|
1854
1944
|
.items([
|
|
1855
|
-
new Image(${s(item.img)}).set({isFull: true, url: ${s(item.img)}}),
|
|
1856
|
-
new Text(${s(item.title)}).set({ size: "S5", color: "#
|
|
1857
|
-
new Link("Link").set({ text: ${s(item.title)}, url: ${s(item.link)}, background: "#
|
|
1945
|
+
new Image(${s(item.img)}).set({isFull: true, url: ${s(item.img)}${item.alt === undefined ? "" : `, alt: ${s(item.alt)}`}}),
|
|
1946
|
+
new Text(${s(item.title)}).set({ size: "S5", tag: "h2", color: "#c2410c" }),
|
|
1947
|
+
new Link("Link").set({ text: ${s(item.title)}, url: ${s(item.link)}, background: "#1d6fe0", pad: [{ lr: "0.5rem", tb: "1rem" }], radius: "0.4rem", color: "white" })
|
|
1858
1948
|
])`;
|
|
1859
1949
|
}
|
|
1860
1950
|
|
|
@@ -2065,7 +2155,7 @@ static form(obj){
|
|
|
2065
2155
|
.filter(l => l.op.name === name)
|
|
2066
2156
|
.filter(l => {
|
|
2067
2157
|
if (l.target) {
|
|
2068
|
-
return l.target.
|
|
2158
|
+
return l.target.some((t) => sameId(t, id));
|
|
2069
2159
|
} else {
|
|
2070
2160
|
return true;
|
|
2071
2161
|
}
|
|
@@ -2116,7 +2206,7 @@ static form(obj){
|
|
|
2116
2206
|
static filteroRaster(id, customOptions) {
|
|
2117
2207
|
const list = (customOptions || []).filter((l) =>
|
|
2118
2208
|
l && typeof l.op === "string" && RASTER_OP_NAMES.includes(l.op) &&
|
|
2119
|
-
(!l.target || (id && l.target.
|
|
2209
|
+
(!l.target || (id && l.target.some((t) => sameId(t, id)))));
|
|
2120
2210
|
return list.length > 0 ? list : undefined;
|
|
2121
2211
|
}
|
|
2122
2212
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
// Every parameter name any element type reads, recovered from source.
|
|
9
9
|
// Used for near-miss typo detection in validate-nodes.js. A drift test
|
|
10
10
|
// regenerates this and fails if it differs, so it cannot rot silently.
|
|
11
|
-
export const ELEMENT_PARAM_NAMES = ["absolute","accept","action","align","alignIts","animate","animation","area","arrayMargin","arrayPadding","at","auto","backTo","background","backgroundCard","backgroundColor","backgroundOp","behaviour","blast","blastData","block","bold","border","borderObj","borderObja","borderRadius","brand","breakWord","breakpoints","cellAlign","cellPadding","center","centerColumn","centered","checkedBackgroundColor","children","clampc","class","clipPath","code","color","column","contentWidth","cursor","customAlign","customJustify","customStyle","data","diameter","disp","dropdown","em","exact","filter","filtera","fixMobileTap","fixed","flex","flexCenter","flexDir","fluid","fluidc","font","frame","ga","gap","gpos","gradient","hamColour","hamburgerColour","headStyle","height","hideField","hover","icon","id","img","index","initLetter","inline","inpad","inputType","isFull","isHidden","isNavA","italic","items","keySet","kind","label","layout","left","length","link","map","mar","margin","marginOp","maxHeight","maxWidth","mboth","method","mobile","multiple","name","new","nowrap","offcanvas","okayColor","onScroll","onTap","opacity","overflow","pad","padding","placeholder","position","positions","preffersId","radius","ranges","raster","react","removeDecoration","required","resmar","respad","responsive","resprop","rounded","scale","sequence","shadow","sides","simpleBorder","simpleCenter","size","span","sticky","stretch","stroke","style","svg","tags","templateCols","text","theme","tint","title","top","transform","transition","type","url","useBrightness","value","vtn","weight","width","x","y","zIndex"];
|
|
11
|
+
export const ELEMENT_PARAM_NAMES = ["absolute","accept","action","align","alignIts","alt","animate","animation","area","arrayMargin","arrayPadding","at","auto","backTo","background","backgroundCard","backgroundColor","backgroundOp","behaviour","blast","blastData","block","bold","border","borderObj","borderObja","borderRadius","brand","breakWord","breakpoints","cellAlign","cellPadding","center","centerColumn","centered","checkedBackgroundColor","children","clampc","class","clipPath","code","color","column","contentWidth","cursor","customAlign","customJustify","customStyle","data","diameter","disp","dropdown","em","exact","filter","filtera","fixMobileTap","fixed","flex","flexCenter","flexDir","fluid","fluidc","font","frame","ga","gap","gpos","gradient","hamColour","hamburgerColour","headStyle","height","hideField","hover","icon","id","img","index","initLetter","inline","inpad","inputType","isFull","isHidden","isNavA","italic","items","keySet","kind","label","layout","left","length","link","map","mar","margin","marginOp","maxHeight","maxWidth","mboth","method","mobile","multiple","name","new","nowrap","offcanvas","okayColor","onScroll","onTap","opacity","overflow","pad","padding","placeholder","position","positions","preffersId","radius","ranges","raster","react","removeDecoration","required","resmar","respad","responsive","resprop","rounded","scale","sequence","shadow","sides","simpleBorder","simpleCenter","size","span","sticky","stretch","stroke","style","svg","tag","tags","templateCols","text","theme","tint","title","top","transform","transition","type","url","useBrightness","value","vtn","weight","width","x","y","zIndex"];
|
package/lib/link-getter.js
CHANGED
package/lib/morph-node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -142,6 +142,13 @@ const norm = (t) => String(t || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
|
142
142
|
const bareId = (id) =>
|
|
143
143
|
typeof id === "string" && id.startsWith("#") ? id.slice(1) : id;
|
|
144
144
|
|
|
145
|
+
// The DOM id a component actually carries, which is not always the bare one:
|
|
146
|
+
// nav and sideNav emit `id="#docs"`, hash included, because animator.js used
|
|
147
|
+
// to require the hash to match. Both spellings are now accepted on both sides,
|
|
148
|
+
// and this is the lookup half — an element is found whether its id was written
|
|
149
|
+
// with the hash or without.
|
|
150
|
+
export const idMatches = (a, b) => bareId(a) === bareId(b);
|
|
151
|
+
|
|
145
152
|
/**
|
|
146
153
|
* A morph node, in one shape: a list of EDGES.
|
|
147
154
|
*
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.3.0
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tier 6 of AGENTIC-FIRST-PLAN.md §10 — accept what the world already writes.
|
|
9
|
+
*
|
|
10
|
+
* MEASURED FIRST. Running claude-sonnet-5 over the 24-brief eval scored 17/24
|
|
11
|
+
* against 23/24 for a hand-written solver, and **four of the seven failures
|
|
12
|
+
* were one mistake wearing different clothes**: the model reached for the name
|
|
13
|
+
* HTML uses and the library wanted another.
|
|
14
|
+
*
|
|
15
|
+
* src -> url an <img> has src. Two briefs lost to this.
|
|
16
|
+
* href -> url an <a> has href.
|
|
17
|
+
* options -> items a <select> has options.
|
|
18
|
+
*
|
|
19
|
+
* These are not typos, so the did-you-mean built in Stage 3 cannot reach them:
|
|
20
|
+
* `src` is three edits from `url`. And a model writing `src` for an image is
|
|
21
|
+
* not confused — it is applying the most widely attested convention there is.
|
|
22
|
+
* The divergence bought nothing and cost three briefs in twenty-four.
|
|
23
|
+
*
|
|
24
|
+
* The same measurement found one more failure of the same shape rather than
|
|
25
|
+
* the same name: a table's rows written as arrays, header row first, which is
|
|
26
|
+
* how every CSV and every markdown table on earth is laid out. That is handled
|
|
27
|
+
* here too — see `normalizeShapes`.
|
|
28
|
+
*
|
|
29
|
+
* Purely additive. A spec that uses the canonical names and shapes is
|
|
30
|
+
* untouched, so no existing page can observe this. Where both are present the canonical name
|
|
31
|
+
* wins and the collision is REPORTED rather than silently resolved, because
|
|
32
|
+
* quietly dropping a key the author wrote is the exact failure this project
|
|
33
|
+
* has spent every stage removing.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** alias -> canonical. Deliberately short: each entry has to earn its place by
|
|
37
|
+
* being what a generator actually writes, not by being plausible. */
|
|
38
|
+
export const ALIASES = Object.freeze({
|
|
39
|
+
src: "url",
|
|
40
|
+
href: "url",
|
|
41
|
+
options: "items",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const CANONICAL = new Set(Object.values(ALIASES));
|
|
45
|
+
|
|
46
|
+
/** Walk children and items as well, so nested specs get the same treatment. */
|
|
47
|
+
const walk = (node, fn) => {
|
|
48
|
+
if (Array.isArray(node)) return node.map((n) => walk(n, fn));
|
|
49
|
+
if (!node || typeof node !== "object") return node;
|
|
50
|
+
const out = fn({ ...node });
|
|
51
|
+
for (const key of ["children", "items"]) {
|
|
52
|
+
if (out[key] !== undefined) out[key] = walk(out[key], fn);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Rewrite every alias to its canonical name. */
|
|
58
|
+
export function normalizeAliases(elements) {
|
|
59
|
+
return walk(elements, (node) => {
|
|
60
|
+
for (const [alias, canonical] of Object.entries(ALIASES)) {
|
|
61
|
+
if (!Object.prototype.hasOwnProperty.call(node, alias)) continue;
|
|
62
|
+
// Canonical wins. `aliasConflicts` is what tells the author.
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(node, canonical)) {
|
|
64
|
+
node[canonical] = node[alias];
|
|
65
|
+
}
|
|
66
|
+
delete node[alias];
|
|
67
|
+
}
|
|
68
|
+
return node;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Places where an alias and its canonical name are both present with
|
|
74
|
+
* DIFFERENT values — one of them is going to be ignored, and the author should
|
|
75
|
+
* hear it from the validator rather than discover it in the page.
|
|
76
|
+
*/
|
|
77
|
+
export function aliasConflicts(elements, at = "elements") {
|
|
78
|
+
const found = [];
|
|
79
|
+
const visit = (node, path) => {
|
|
80
|
+
if (Array.isArray(node)) return node.forEach((n, i) => visit(n, `${path}[${i}]`));
|
|
81
|
+
if (!node || typeof node !== "object") return;
|
|
82
|
+
for (const [alias, canonical] of Object.entries(ALIASES)) {
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(node, alias) &&
|
|
84
|
+
Object.prototype.hasOwnProperty.call(node, canonical) &&
|
|
85
|
+
JSON.stringify(node[alias]) !== JSON.stringify(node[canonical])) {
|
|
86
|
+
found.push({ path: `${path}.${alias}`, alias, canonical });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const key of ["children", "items"]) {
|
|
90
|
+
if (node[key] !== undefined) visit(node[key], `${path}.${key}`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
visit(elements, at);
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every name this module accepts, for the validator's vocabulary. */
|
|
98
|
+
export const ALIAS_NAMES = Object.freeze(Object.keys(ALIASES));
|
|
99
|
+
export const isCanonical = (name) => CANONICAL.has(name);
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Shapes that are not the library's but are what everyone writes.
|
|
104
|
+
*
|
|
105
|
+
* A table's rows as ARRAYS, header row first:
|
|
106
|
+
*
|
|
107
|
+
* items: [["date","race"], ["29/08","Krusnoman"]]
|
|
108
|
+
* -> items: [{ date: "29/08", race: "Krusnoman" }]
|
|
109
|
+
*
|
|
110
|
+
* The model in the Tier 2b run wrote exactly this, and it is the layout of
|
|
111
|
+
* every CSV and every markdown table there is. Header-first is not a guess:
|
|
112
|
+
* a table whose first row is data has no column names at all, and the mapper
|
|
113
|
+
* needs them to build the head.
|
|
114
|
+
*
|
|
115
|
+
* Narrow on purpose — `table` only, and only when EVERY entry is an array.
|
|
116
|
+
* A mixed list is left alone rather than half-converted.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeShapes(elements) {
|
|
119
|
+
return walk(elements, (node) => {
|
|
120
|
+
if (node.type !== "table" || !Array.isArray(node.items) || node.items.length < 2) return node;
|
|
121
|
+
if (!node.items.every((r) => Array.isArray(r))) return node;
|
|
122
|
+
|
|
123
|
+
const [head, ...rows] = node.items;
|
|
124
|
+
if (!head.every((h) => typeof h === "string" || typeof h === "number")) return node;
|
|
125
|
+
node.items = rows.map((row) =>
|
|
126
|
+
Object.fromEntries(head.map((key, i) => [String(key), row[i]])));
|
|
127
|
+
return node;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Both passes, in the order the caller wants: names first, then shapes. */
|
|
132
|
+
export function normalizeSpec(elements) {
|
|
133
|
+
return normalizeShapes(normalizeAliases(elements));
|
|
134
|
+
}
|
package/lib/parse-html.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
* this stage across mixed leaf-and-composite specs.
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
+
import { validateNodes } from "./validate-nodes.js";
|
|
51
|
+
|
|
50
52
|
const HEADINGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
51
53
|
|
|
52
54
|
/** The attribute a page uses to carry its own descriptors. */
|
|
@@ -129,11 +131,21 @@ export function parseReport(html, doc) {
|
|
|
129
131
|
if (one) spec.push(one);
|
|
130
132
|
else unrecovered.push({ index: i, tag: (node.tagName || "").toLowerCase() });
|
|
131
133
|
});
|
|
134
|
+
// What parse returns is UNTRUSTED. A `data-nod` attribute is just text in
|
|
135
|
+
// a document: hand-edited, served by someone else, or written by a version
|
|
136
|
+
// of the library that is not this one. The caller's next move is to render
|
|
137
|
+
// it, so the recovered spec is checked here rather than after it has
|
|
138
|
+
// already become a page. This is also the first place Stage 3 and Stage 5
|
|
139
|
+
// compose — the validator that makes a spec repairable is the one that
|
|
140
|
+
// makes a parsed spec safe.
|
|
141
|
+
const check = validateNodes([], spec);
|
|
142
|
+
|
|
132
143
|
return {
|
|
133
|
-
ok: unrecovered.length === 0,
|
|
144
|
+
ok: unrecovered.length === 0 && check.ok,
|
|
134
145
|
exact: kids.every((n) => n.getAttribute && n.getAttribute(SPEC_ATTR)),
|
|
135
146
|
total: kids.length,
|
|
136
147
|
spec,
|
|
137
148
|
unrecovered,
|
|
149
|
+
errors: check.errors,
|
|
138
150
|
};
|
|
139
151
|
}
|
package/lib/raster-inspect.js
CHANGED
package/lib/raster-ops.js
CHANGED
package/lib/raster-presets.js
CHANGED