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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.3.0
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tier 3 of AGENTIC-FIRST-PLAN.md §10 — declare shared structure once.
|
|
9
|
+
*
|
|
10
|
+
* MEASURED FIRST, as the plan asked. Chrome taken from a real site: a nav and
|
|
11
|
+
* a footer are 184 tokens, and **61% of every token in that site is the same
|
|
12
|
+
* nav and footer**. Referencing rather than repeating them is 1.98x at ten
|
|
13
|
+
* pages and 2.10x at twenty — and a NET LOSS at one page, which is why this is
|
|
14
|
+
* opt-in and why nothing changes for a caller who never uses it.
|
|
15
|
+
*
|
|
16
|
+
* The reason it is needed is not that the data format is verbose. The site
|
|
17
|
+
* used for that measurement does not repeat itself: it imports `renderNav`
|
|
18
|
+
* from a shared module and calls it. That is the imperative layer, where reuse
|
|
19
|
+
* is just a function. An element array cannot call a function, so an agent
|
|
20
|
+
* authoring as data pays for the chrome once per page, and the only way out is
|
|
21
|
+
* to drop into code — which is what property 1, "total", exists to prevent.
|
|
22
|
+
* A human never feels this and an agent always does.
|
|
23
|
+
*
|
|
24
|
+
* new Des()
|
|
25
|
+
* .defs({ nav: { type: "nav", items: [...] } })
|
|
26
|
+
* .add([{ $ref: "nav" }, { type: "h1", text: "Page" }])
|
|
27
|
+
*
|
|
28
|
+
* The definitions are ordinary data, so they serialise, ship in one file, and
|
|
29
|
+
* are imported by every page's entry — the sharing is a plain import of a
|
|
30
|
+
* plain object, not a new authoring mode.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The key that marks a reference. `$`-prefixed so it cannot collide with a
|
|
34
|
+
* parameter name: the schema's vocabulary is all bare identifiers. */
|
|
35
|
+
export const REF_KEY = "$ref";
|
|
36
|
+
|
|
37
|
+
/** True for `{ $ref: "name" }` and nothing else. */
|
|
38
|
+
export const isRef = (v) =>
|
|
39
|
+
!!v && typeof v === "object" && !Array.isArray(v) && typeof v[REF_KEY] === "string";
|
|
40
|
+
|
|
41
|
+
/** Every name referenced anywhere in a spec, with the path that reached it. */
|
|
42
|
+
export function collectRefs(elements, at = "elements") {
|
|
43
|
+
const found = [];
|
|
44
|
+
const walk = (node, path) => {
|
|
45
|
+
if (Array.isArray(node)) return node.forEach((n, i) => walk(n, `${path}[${i}]`));
|
|
46
|
+
if (!node || typeof node !== "object") return;
|
|
47
|
+
if (isRef(node)) { found.push({ name: node[REF_KEY], path }); return; }
|
|
48
|
+
for (const key of ["children", "items"]) {
|
|
49
|
+
if (node[key] !== undefined) walk(node[key], `${path}.${key}`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
walk(elements, at);
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Replace every `{ $ref: name }` with a deep copy of `defs[name]`.
|
|
58
|
+
*
|
|
59
|
+
* A copy, not the definition itself: two pages referencing one nav must not
|
|
60
|
+
* share an object the mapper then mutates — rendering an `a` already adds
|
|
61
|
+
* `font` and `pad` to the descriptor it is given, so a shared definition would
|
|
62
|
+
* accumulate the leavings of every page that used it.
|
|
63
|
+
*
|
|
64
|
+
* An unresolvable reference is left in place rather than dropped or guessed.
|
|
65
|
+
* The validator reports it as DANGLING_REF with a did-you-mean; silently
|
|
66
|
+
* removing it would make a missing nav look like a page that never had one.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveRefs(elements, defs, seen = new Set()) {
|
|
69
|
+
if (!defs || typeof defs !== "object") return elements;
|
|
70
|
+
|
|
71
|
+
const expand = (node) => {
|
|
72
|
+
if (Array.isArray(node)) return node.map(expand);
|
|
73
|
+
if (!node || typeof node !== "object") return node;
|
|
74
|
+
|
|
75
|
+
if (isRef(node)) {
|
|
76
|
+
const name = node[REF_KEY];
|
|
77
|
+
if (!Object.prototype.hasOwnProperty.call(defs, name)) return node;
|
|
78
|
+
if (seen.has(name)) {
|
|
79
|
+
// A definition that reaches itself. Returning the ref rather
|
|
80
|
+
// than recursing means a cycle is a reported error instead of
|
|
81
|
+
// a stack overflow at render time.
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
const next = new Set(seen).add(name);
|
|
85
|
+
const base = resolveRefs([defs[name]], defs, next)[0];
|
|
86
|
+
|
|
87
|
+
// Keys written BESIDE the reference override the definition.
|
|
88
|
+
//
|
|
89
|
+
// Without this they were silently dropped — `{ $ref: "nav", id:
|
|
90
|
+
// "topnav" }` expanded to the bare definition and the id went
|
|
91
|
+
// nowhere, which is precisely the declared-but-ignored failure
|
|
92
|
+
// this project spent six stages removing. Having built a new one,
|
|
93
|
+
// the choice was to reject the shape or to honour it; honouring it
|
|
94
|
+
// is what makes a shared fragment reusable at all, since the whole
|
|
95
|
+
// point of sharing a nav is that each page marks a different entry.
|
|
96
|
+
//
|
|
97
|
+
// Shallow, deliberately: a deep merge would make it unclear
|
|
98
|
+
// whether `items` replaces the definition's or extends it. It
|
|
99
|
+
// replaces.
|
|
100
|
+
const { [REF_KEY]: _drop, ...overrides } = node;
|
|
101
|
+
return Object.keys(overrides).length
|
|
102
|
+
? { ...(base && typeof base === "object" ? base : {}), ...overrides }
|
|
103
|
+
: base;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const out = { ...node };
|
|
107
|
+
for (const key of ["children", "items"]) {
|
|
108
|
+
if (out[key] !== undefined) out[key] = expand(out[key]);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return Array.isArray(elements) ? elements.map(expand) : elements;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Names defined but never referenced — dead weight in a shared file. */
|
|
117
|
+
export function unusedDefs(elements, defs) {
|
|
118
|
+
if (!defs) return [];
|
|
119
|
+
const used = new Set(collectRefs(elements).map((r) => r.name));
|
|
120
|
+
return Object.keys(defs).filter((n) => !used.has(n));
|
|
121
|
+
}
|
package/lib/scroll-video.js
CHANGED
package/lib/seo.js
CHANGED
package/lib/stacker.js
CHANGED
package/lib/suggest.js
CHANGED
package/lib/theme.js
CHANGED
package/lib/transform-anim.js
CHANGED
package/lib/transition.js
CHANGED
package/lib/validate-nodes.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
|
*/
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
FRAMEWORK_DOC, EASING_NAMES,
|
|
31
31
|
} from "./raster-ops.js";
|
|
32
32
|
import { presetNames, presetInfo } from "./raster-presets.js";
|
|
33
|
+
import { collectRefs, isRef, resolveRefs } from "./resolve-refs.js";
|
|
34
|
+
import { normalizeSpec, aliasConflicts, ALIASES } from "./normalize-spec.js";
|
|
33
35
|
import { ELEMENT_TYPES } from "./element-mapper.js";
|
|
34
36
|
import { ELEMENT_PARAM_NAMES } from "./element-params.generated.js";
|
|
35
37
|
import { didYouMean, suggest, levenshtein } from "./suggest.js";
|
|
@@ -141,7 +143,27 @@ const familyOf = (node) => {
|
|
|
141
143
|
* which is the other half of the "silent no-op" problem: a correctly
|
|
142
144
|
* spelled op aimed at an element that is not there does nothing at all.
|
|
143
145
|
*/
|
|
144
|
-
|
|
146
|
+
// Which key actually carries a composite's content.
|
|
147
|
+
//
|
|
148
|
+
// GROUND TRUTH, DERIVED BY RENDERING, not by reading the source. A static scan
|
|
149
|
+
// got this wrong: `mapRow` appears to read `items` because a helper it calls
|
|
150
|
+
// does, and that helper's `items` belongs to a different element entirely. So
|
|
151
|
+
// each type was rendered with a distinctive payload in `items` and again in
|
|
152
|
+
// `children`, and only the slot the output actually followed is recorded here.
|
|
153
|
+
// `__tests__/unit/content-slots.test.mjs` re-derives it the same way and fails
|
|
154
|
+
// if this table drifts, which is the arrangement Stage 2 established for the
|
|
155
|
+
// schema.
|
|
156
|
+
//
|
|
157
|
+
// ONLY types with positive evidence appear. A type whose content slot could not
|
|
158
|
+
// be demonstrated is absent, and absent means silent: reporting a slot that
|
|
159
|
+
// might work would stop `preview` rendering a page that renders, which 1.2.7
|
|
160
|
+
// established is the costlier direction to be wrong in.
|
|
161
|
+
export const CONTENT_SLOT = {
|
|
162
|
+
cards: "items", nav: "items", sideNav: "items", table: "items", ulist: "items",
|
|
163
|
+
row: "children", form: "children", wrap: "children",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export function validateNodes(nodes, elements, defs) {
|
|
145
167
|
const errors = [];
|
|
146
168
|
const push = (code, path, got, suggestions, valid) => {
|
|
147
169
|
errors.push({
|
|
@@ -199,6 +221,42 @@ export function validateNodes(nodes, elements) {
|
|
|
199
221
|
// element whose `type` names nothing is not a silent no-op but a THROW
|
|
200
222
|
// from the mapper at render time, which reaches a generator as a stack
|
|
201
223
|
// trace rather than as a report it can repair from.
|
|
224
|
+
// Tier 3: a reference with no definition. Reported here as well as
|
|
225
|
+
// refused at render, because a report is repairable in one turn and a
|
|
226
|
+
// throw is not — the Stage 3 contract applied to the newest way of being
|
|
227
|
+
// wrong. `defs` is optional: without it references are left alone rather
|
|
228
|
+
// than all reported missing, since a caller may validate a page before
|
|
229
|
+
// deciding what to share.
|
|
230
|
+
if (defs && typeof defs === "object" && Array.isArray(elements)) {
|
|
231
|
+
const defined = Object.keys(defs);
|
|
232
|
+
for (const { name, path } of collectRefs(elements)) {
|
|
233
|
+
if (!Object.prototype.hasOwnProperty.call(defs, name)) {
|
|
234
|
+
push("DANGLING_REF", path, name, suggest(name, defined), defined);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Walk the RESOLVED tree when definitions are available. A reference
|
|
240
|
+
// with keys beside it is a real element once merged, and only the merged
|
|
241
|
+
// form can be checked: `{ $ref: "nav", itms: [] }` is a typo in an
|
|
242
|
+
// override, and validating the unexpanded reference would miss it —
|
|
243
|
+
// a silent no-op inside the mechanism built to remove silent no-ops.
|
|
244
|
+
let toWalk = (defs && typeof defs === "object" && Array.isArray(elements))
|
|
245
|
+
? resolveRefs(elements, defs)
|
|
246
|
+
: elements;
|
|
247
|
+
|
|
248
|
+
// An alias and its canonical name both present, disagreeing: one of them
|
|
249
|
+
// is going to be ignored. The author hears it here rather than finding it
|
|
250
|
+
// in the page.
|
|
251
|
+
for (const c of aliasConflicts(Array.isArray(elements) ? elements : [])) {
|
|
252
|
+
push("CONFLICTING_ALIAS", c.path, c.alias, [c.canonical],
|
|
253
|
+
[`"${c.alias}" is accepted as "${c.canonical}"; give one of them, not both`]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Walk the normalised tree, so `{type:"img", src:"..."}` is checked as the
|
|
257
|
+
// element it actually becomes rather than reported as an unknown parameter.
|
|
258
|
+
if (Array.isArray(toWalk)) toWalk = normalizeSpec(toWalk);
|
|
259
|
+
|
|
202
260
|
if (idsKnown) {
|
|
203
261
|
const walkEls = (list, at) => {
|
|
204
262
|
if (!Array.isArray(list)) return;
|
|
@@ -219,6 +277,12 @@ export function validateNodes(nodes, elements) {
|
|
|
219
277
|
push("BAD_ELEMENT", p, el, [], ["{ type: \"<element type>\", ... }"]);
|
|
220
278
|
return;
|
|
221
279
|
}
|
|
280
|
+
// A reference is not an element and has no `type` by design.
|
|
281
|
+
// Its validity is whether the name resolves, which the
|
|
282
|
+
// DANGLING_REF pass above decides; reporting MISSING_FIELD
|
|
283
|
+
// here rejected every correct `$ref` there is.
|
|
284
|
+
if (isRef(el)) return;
|
|
285
|
+
|
|
222
286
|
if (typeof el.type !== "string") {
|
|
223
287
|
push("MISSING_FIELD", `${p}.type`, el.type, [],
|
|
224
288
|
["an element type, e.g. \"h1\", \"wrap\", \"nav\""]);
|
|
@@ -226,6 +290,38 @@ export function validateNodes(nodes, elements) {
|
|
|
226
290
|
push("UNKNOWN_ELEMENT_TYPE", `${p}.type`, el.type,
|
|
227
291
|
suggest(el.type, ELEMENT_TYPES), ELEMENT_TYPES);
|
|
228
292
|
}
|
|
293
|
+
// Content declared in the slot this type does not read.
|
|
294
|
+
// `{type:"table", children:[...]}` validated clean and then
|
|
295
|
+
// rendered its placeholders — the exact silent no-op Stage 1
|
|
296
|
+
// removed for `cards`, surviving one slot over. It is not a
|
|
297
|
+
// typo, so near-miss detection cannot reach it.
|
|
298
|
+
const wants = CONTENT_SLOT[el.type];
|
|
299
|
+
if (wants) {
|
|
300
|
+
// A content slot that is present but not a list. The
|
|
301
|
+
// mapper tests `Array.isArray(...) && length`, so
|
|
302
|
+
// `items: {…}` and `items: "x"` fall through to the
|
|
303
|
+
// placeholders — content declared, content ignored, no
|
|
304
|
+
// error. Same family as WRONG_CONTENT_SLOT: the key is
|
|
305
|
+
// right and the value cannot carry anything.
|
|
306
|
+
const given = el[wants];
|
|
307
|
+
if (given !== undefined && given !== null && !Array.isArray(given)) {
|
|
308
|
+
push("BAD_CONTENT_SHAPE", `${p}.${wants}`, given, [],
|
|
309
|
+
[`an array — "${el.type}" reads \`${wants}\` as a list`]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (wants) {
|
|
313
|
+
const other = wants === "items" ? "children" : "items";
|
|
314
|
+
const filled = (v) => Array.isArray(v) ? v.length > 0 : v != null;
|
|
315
|
+
// All-strings is the pre-S1 gesture and already gets its own
|
|
316
|
+
// diagnostic from the walk below; do not report it twice.
|
|
317
|
+
const allStrings = Array.isArray(el[other])
|
|
318
|
+
&& el[other].every((x) => typeof x === "string");
|
|
319
|
+
if (filled(el[other]) && !filled(el[wants]) && !allStrings) {
|
|
320
|
+
push("WRONG_CONTENT_SLOT", `${p}.${other}`, other, [wants],
|
|
321
|
+
[`"${el.type}" carries its content in \`${wants}\``]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
229
325
|
walkEls(el.children, `${p}.children`);
|
|
230
326
|
|
|
231
327
|
// Phase S3: a misspelled element parameter. `{type:"cards",
|
|
@@ -288,7 +384,7 @@ export function validateNodes(nodes, elements) {
|
|
|
288
384
|
}
|
|
289
385
|
});
|
|
290
386
|
};
|
|
291
|
-
walkEls(
|
|
387
|
+
walkEls(toWalk, "elements");
|
|
292
388
|
}
|
|
293
389
|
|
|
294
390
|
nodes.forEach((node, i) => {
|
package/lib/webmcp-adapter.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodality",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A lightweight library for declarative UI elements.",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"bin/",
|
|
15
15
|
"API.md",
|
|
16
16
|
"examples/",
|
|
17
|
-
"skills/"
|
|
17
|
+
"skills/",
|
|
18
|
+
"schema.json",
|
|
19
|
+
"scripts/generate-schema.mjs"
|
|
18
20
|
],
|
|
19
21
|
"exports": {
|
|
20
22
|
".": {
|
|
@@ -31,6 +33,8 @@
|
|
|
31
33
|
"./raster": "./lib/raster-ops.js",
|
|
32
34
|
"./validate": "./lib/validate-nodes.js",
|
|
33
35
|
"./parse": "./lib/parse-html.js",
|
|
36
|
+
"./refs": "./lib/resolve-refs.js",
|
|
37
|
+
"./normalize": "./lib/normalize-spec.js",
|
|
34
38
|
"./inspect": "./lib/raster-inspect.js",
|
|
35
39
|
"./examples/*": "./examples/*.js",
|
|
36
40
|
"./transition": "./lib/transition.js",
|