nodality 1.1.14 → 1.2.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 +41 -5
- package/bin/nodality.js +61 -0
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +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 +60 -1
- package/dist/button.cjs.js.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js.LICENSE.txt +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 +60 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +60 -1
- 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 +60 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js.LICENSE.txt +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 +60 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +60 -1
- 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.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +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.LICENSE.txt +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.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +1 -1
- 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 +1 -1
- 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 +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +6 -1
- package/layout/form-components/image-picker.js +1 -1
- 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 +1 -1
- 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 +1 -1
- package/layout/side-nav-bar.js +1 -1
- 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 +13 -1
- package/layout/text.js +1 -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 +347 -0
- package/lib/card-getter.js +1 -1
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +22 -1
- package/lib/element-mapper.js +2 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +47 -8
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +1 -1
- package/lib/raster-presets.js +1 -1
- 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 +196 -32
- package/lib/webmcp-adapter.js +307 -0
- package/package.json +1 -1
package/lib/link-getter.js
CHANGED
package/lib/morph-node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.2.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -123,6 +123,25 @@ const chainFor = (effect) => {
|
|
|
123
123
|
|
|
124
124
|
const norm = (t) => String(t || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* An id as written, reduced to the id itself.
|
|
128
|
+
*
|
|
129
|
+
* A node may name a state bare (`"home"`) or in selector form
|
|
130
|
+
* (`"#home"`). Both are accepted and both mean the same thing, because
|
|
131
|
+
* this is an element IDENTIFIER rather than a selector: the lookup is
|
|
132
|
+
* `getElementById`, plus a positional fallback against `E`, so `"#home"`
|
|
133
|
+
* would otherwise match nothing and the morph would fail with a console
|
|
134
|
+
* warning and no visible effect.
|
|
135
|
+
*
|
|
136
|
+
* Normalised HERE, once, rather than at the point of lookup: the states
|
|
137
|
+
* are keyed by this string, so a chain whose first edge writes `"#home"`
|
|
138
|
+
* and whose second writes `"home"` must arrive at ONE key. Stripping at
|
|
139
|
+
* lookup time would resolve both to the same element while keeping two
|
|
140
|
+
* entries in the map, and the second edge would then be unreachable.
|
|
141
|
+
*/
|
|
142
|
+
const bareId = (id) =>
|
|
143
|
+
typeof id === "string" && id.startsWith("#") ? id.slice(1) : id;
|
|
144
|
+
|
|
126
145
|
/**
|
|
127
146
|
* A morph node, in one shape: a list of EDGES.
|
|
128
147
|
*
|
|
@@ -170,19 +189,22 @@ const normalizeEdges = (m) => {
|
|
|
170
189
|
// Labels are what the user is actually reading and clicking.
|
|
171
190
|
const byLabel = e.to && !Array.isArray(e.to) && typeof e.to === "object"
|
|
172
191
|
? e.to : null;
|
|
192
|
+
// Deduplicated on the label side only. The array form is POSITIONAL
|
|
193
|
+
// — `toIds[triggers.indexOf(a)]` below — so collapsing a repeated
|
|
194
|
+
// id there would shift every later link onto the wrong destination.
|
|
173
195
|
const toIds = byLabel
|
|
174
|
-
? [...new Set(Object.values(e.to))]
|
|
175
|
-
: (Array.isArray(e.to) ? e.to : [e.to]);
|
|
196
|
+
? [...new Set(Object.values(e.to))].map(bareId)
|
|
197
|
+
: (Array.isArray(e.to) ? e.to : [e.to]).map(bareId);
|
|
176
198
|
|
|
177
199
|
const labelToId = new Map();
|
|
178
200
|
if (byLabel) {
|
|
179
201
|
for (const [label, id] of Object.entries(e.to)) {
|
|
180
|
-
labelToId.set(norm(label), id);
|
|
202
|
+
labelToId.set(norm(label), bareId(id));
|
|
181
203
|
}
|
|
182
204
|
}
|
|
183
205
|
|
|
184
206
|
return {
|
|
185
|
-
from: e.from, byLabel, toIds, labelToId,
|
|
207
|
+
from: bareId(e.from), byLabel, toIds, labelToId,
|
|
186
208
|
effect: opt("effect"),
|
|
187
209
|
duration: opt("duration", 900),
|
|
188
210
|
// Written as `!== false` rather than a truthy test: the
|
|
@@ -212,9 +234,13 @@ function applyMorphNodes(mount, elements, nodes) {
|
|
|
212
234
|
// page. Position is what Des actually guarantees.
|
|
213
235
|
const rendered = [...mount.children];
|
|
214
236
|
const resolve = (id) => {
|
|
215
|
-
|
|
237
|
+
// `bareId` again, and not only for symmetry: the descriptor in E
|
|
238
|
+
// may itself be written with a leading "#", so both sides are
|
|
239
|
+
// reduced before they are compared.
|
|
240
|
+
const want = bareId(id);
|
|
241
|
+
const byId = document.getElementById(want);
|
|
216
242
|
if (byId && mount.contains(byId)) return byId;
|
|
217
|
-
const i = (elements || []).findIndex((e) => e && e.id ===
|
|
243
|
+
const i = (elements || []).findIndex((e) => e && bareId(e.id) === want);
|
|
218
244
|
return i >= 0 ? rendered[i] : null;
|
|
219
245
|
};
|
|
220
246
|
|
|
@@ -811,6 +837,13 @@ function setUpMorph(mount, m, resolve) {
|
|
|
811
837
|
get active() { return active; },
|
|
812
838
|
goTo, goToState,
|
|
813
839
|
back() { if (tl) tl.to(0); },
|
|
840
|
+
// The control's own entry point, published for the agent surface.
|
|
841
|
+
// `back()` above reverses a RUNNING timeline and does nothing at
|
|
842
|
+
// rest; `goBack` is what the back button calls — queued, one
|
|
843
|
+
// press one level, reusing the live pipeline or rebuilding it.
|
|
844
|
+
// A derived go_back tool has to be the same action as the button
|
|
845
|
+
// or the two would unwind differently from the same state.
|
|
846
|
+
goBack,
|
|
814
847
|
destroy() {
|
|
815
848
|
if (tl) tl.destroy();
|
|
816
849
|
for (const p of activeRasterPipelines()) {
|
|
@@ -821,4 +854,10 @@ function setUpMorph(mount, m, resolve) {
|
|
|
821
854
|
};
|
|
822
855
|
}
|
|
823
856
|
|
|
824
|
-
|
|
857
|
+
// `normalizeEdges` is exported for DERIVATION, not for use: the agent
|
|
858
|
+
// surface reads the same edge list the controller runs, so the two can
|
|
859
|
+
// never describe different graphs. Reimplementing the normalisation
|
|
860
|
+
// there would have drifted the first time either side changed — which
|
|
861
|
+
// is precisely how the validator came to accept a `#id` the runtime
|
|
862
|
+
// refused to resolve.
|
|
863
|
+
export { applyMorphNodes, normalizeEdges };
|
package/lib/raster-inspect.js
CHANGED
package/lib/raster-ops.js
CHANGED
package/lib/raster-presets.js
CHANGED
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.2.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -36,15 +36,36 @@ import { didYouMean, suggest } from "./suggest.js";
|
|
|
36
36
|
* the runtime, which is why it has to be reported here.
|
|
37
37
|
*/
|
|
38
38
|
const MORPH_FIELDS = [
|
|
39
|
-
"op", "target", "from", "to",
|
|
39
|
+
"op", "target", "from", "to", "chain",
|
|
40
40
|
"effect", "duration", "back", "live", "fade",
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Every key an entry in `chain` reads. `from` and `to` make the edge; the
|
|
45
|
+
* rest are the node's own settings, overridable per edge — which is why
|
|
46
|
+
* this list is MORPH_FIELDS without `op`, `target` or `chain`, an edge
|
|
47
|
+
* being able to carry neither an op nor a nested chain of its own.
|
|
48
|
+
*/
|
|
49
|
+
const EDGE_FIELDS = [
|
|
50
|
+
"from", "to", "effect", "duration", "back", "live", "fade",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every key an `agent-surface` node reads. The node is small on purpose:
|
|
55
|
+
* what it exposes is DERIVED, so there is nothing here to describe the
|
|
56
|
+
* tools with — only which parts of the page may be reached.
|
|
57
|
+
*/
|
|
58
|
+
const AGENT_SURFACE_FIELDS = ["op", "name", "forms", "exclude"];
|
|
59
|
+
|
|
43
60
|
/** Node families, told apart exactly the way the runtime tells them apart. */
|
|
44
61
|
const familyOf = (node) => {
|
|
45
62
|
if (!node || typeof node !== "object") return "invalid";
|
|
46
63
|
const op = node.op;
|
|
47
|
-
if (typeof op === "string")
|
|
64
|
+
if (typeof op === "string") {
|
|
65
|
+
if (op === "morph") return "morph";
|
|
66
|
+
if (op === "agent-surface") return "agent-surface";
|
|
67
|
+
return "raster";
|
|
68
|
+
}
|
|
48
69
|
if (op && typeof op === "object" && typeof op.name === "string") return "design";
|
|
49
70
|
return "invalid";
|
|
50
71
|
};
|
|
@@ -96,8 +117,13 @@ export function validateNodes(nodes, elements) {
|
|
|
96
117
|
push("BAD_TARGET", at, t, [], ["an element id"]);
|
|
97
118
|
return;
|
|
98
119
|
}
|
|
99
|
-
//
|
|
100
|
-
// "#"
|
|
120
|
+
// An id may be written bare ("home") or in selector form
|
|
121
|
+
// ("#home"). Both are accepted here because both are accepted
|
|
122
|
+
// by the runtime — `bareId` in morph-node.js reduces them to
|
|
123
|
+
// one key before anything is resolved. The two must agree: for
|
|
124
|
+
// a while this check tolerated "#home" while the runtime did
|
|
125
|
+
// not, so the validator passed a node that silently never
|
|
126
|
+
// morphed, which is the exact failure it exists to prevent.
|
|
101
127
|
const bare = t.startsWith("#") ? t.slice(1) : t;
|
|
102
128
|
if (idsKnown && !ids.includes(bare)) {
|
|
103
129
|
push("UNKNOWN_TARGET", at, t, suggest(bare, ids), ids);
|
|
@@ -120,38 +146,176 @@ export function validateNodes(nodes, elements) {
|
|
|
120
146
|
|
|
121
147
|
if (family === "design") return; // design nodes carry their own vocabulary
|
|
122
148
|
|
|
123
|
-
if (family === "
|
|
124
|
-
// The
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
if (family === "agent-surface") {
|
|
150
|
+
// The ids this node names must exist, for the usual reason:
|
|
151
|
+
// a surface that silently exposes nothing is the agentic
|
|
152
|
+
// form of the silent no-op, and an agent has no way to see
|
|
153
|
+
// that the tool it expected was never registered.
|
|
154
|
+
const formIds = [];
|
|
155
|
+
const walkForms = (list) => {
|
|
156
|
+
if (!Array.isArray(list)) return;
|
|
157
|
+
for (const el of list) {
|
|
158
|
+
if (!el || typeof el !== "object") continue;
|
|
159
|
+
if (el.type === "form" && typeof el.id === "string") formIds.push(el.id);
|
|
160
|
+
walkForms(el.children);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
if (idsKnown) walkForms(elements);
|
|
164
|
+
|
|
165
|
+
if (node.name !== undefined && typeof node.name !== "string") {
|
|
166
|
+
push("BAD_FIELD", `${path}.name`, node.name, [],
|
|
167
|
+
["a string used to prefix every derived tool name"]);
|
|
131
168
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
for (const label in node.to) {
|
|
139
|
-
checkTargets(node.to[label], `${path}.to.${label}`);
|
|
169
|
+
|
|
170
|
+
for (const key of ["forms", "exclude"]) {
|
|
171
|
+
if (node[key] === undefined) continue;
|
|
172
|
+
if (!Array.isArray(node[key])) {
|
|
173
|
+
push("BAD_FIELD", `${path}.${key}`, node[key], [], ["an array of ids"]);
|
|
174
|
+
continue;
|
|
140
175
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
node[key].forEach((v, k) => {
|
|
177
|
+
const at = `${path}.${key}[${k}]`;
|
|
178
|
+
if (typeof v !== "string") {
|
|
179
|
+
push("BAD_TARGET", at, v, [], ["an element id"]);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const bare = v.startsWith("#") ? v.slice(1) : v;
|
|
183
|
+
if (key === "forms") {
|
|
184
|
+
// Checked against FORMS, not against every id: a
|
|
185
|
+
// node naming a heading would otherwise pass here
|
|
186
|
+
// and derive no tool at all.
|
|
187
|
+
if (idsKnown && !formIds.includes(bare)) {
|
|
188
|
+
push("UNKNOWN_FORM", at, v, suggest(bare, formIds), formIds);
|
|
189
|
+
}
|
|
190
|
+
} else if (idsKnown && !ids.includes(bare)) {
|
|
191
|
+
push("UNKNOWN_STATE", at, v, suggest(bare, ids), ids);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A surface with no graph and no forms still derives
|
|
197
|
+
// `read_view`, which is legitimate — a page can be worth
|
|
198
|
+
// reading and nothing else. Reported so that a node written
|
|
199
|
+
// in the expectation of more says so.
|
|
200
|
+
const hasChain = nodes.some((n) => n && n.op === "morph");
|
|
201
|
+
const hasForms = Array.isArray(node.forms) && node.forms.length > 0;
|
|
202
|
+
if (!hasChain && !hasForms) {
|
|
203
|
+
push("EMPTY_SURFACE", path, node, [],
|
|
204
|
+
["add a { op: \"morph\" } node, or name a form in `forms`"]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const key in node) {
|
|
208
|
+
if (AGENT_SURFACE_FIELDS.includes(key)) continue;
|
|
209
|
+
push("UNKNOWN_PARAM", `${path}.${key}`, key,
|
|
210
|
+
suggest(key, AGENT_SURFACE_FIELDS), AGENT_SURFACE_FIELDS);
|
|
144
211
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (family === "morph") {
|
|
216
|
+
// `from` and `to` are what make a hop; without them it is
|
|
217
|
+
// inert. Factored out because a chain edge has exactly the
|
|
218
|
+
// same two ends as a single-hop node, and the diagnostics
|
|
219
|
+
// should not differ by where the edge was written.
|
|
220
|
+
const checkEnds = (o, at) => {
|
|
221
|
+
if (typeof o.from !== "string") {
|
|
222
|
+
push("MISSING_FIELD", `${at}.from`, o.from, [],
|
|
223
|
+
["the id of the element that morphs"]);
|
|
224
|
+
} else {
|
|
225
|
+
checkTargets(o.from, `${at}.from`);
|
|
226
|
+
}
|
|
227
|
+
if (o.to === undefined) {
|
|
228
|
+
push("MISSING_FIELD", `${at}.to`, undefined, [],
|
|
229
|
+
["[\"id\", ...]", "{ \"Link label\": \"id\" }"]);
|
|
230
|
+
} else if (Array.isArray(o.to)) {
|
|
231
|
+
checkTargets(o.to, `${at}.to`);
|
|
232
|
+
} else if (o.to && typeof o.to === "object") {
|
|
233
|
+
for (const label in o.to) {
|
|
234
|
+
checkTargets(o.to[label], `${at}.to.${label}`);
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
push("BAD_FIELD", `${at}.to`, o.to, [],
|
|
238
|
+
["an array of ids, or a map of link label to id"]);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// The settings, which are hoisted defaults on the node and
|
|
243
|
+
// overrides on an edge, so both are checked the same way.
|
|
244
|
+
// `back` and `live` are compared against `false`/`true` by
|
|
245
|
+
// identity in the runtime, which makes a STRING the dangerous
|
|
246
|
+
// mistake: `live: "true"` is not `=== true`, so the live
|
|
247
|
+
// backend silently stays off and the page still works, on the
|
|
248
|
+
// other backend, looking as though the flag took.
|
|
249
|
+
const checkOpts = (o, at) => {
|
|
250
|
+
if (o.duration !== undefined &&
|
|
251
|
+
(typeof o.duration !== "number" || !(o.duration > 0))) {
|
|
252
|
+
push("BAD_VALUE", `${at}.duration`, o.duration, [],
|
|
253
|
+
["a positive number of milliseconds"]);
|
|
254
|
+
}
|
|
255
|
+
if (o.effect !== undefined &&
|
|
256
|
+
typeof o.effect !== "string" && !Array.isArray(o.effect)) {
|
|
257
|
+
push("BAD_FIELD", `${at}.effect`, o.effect, [],
|
|
258
|
+
["a preset name, or an inline array of raster nodes"]);
|
|
259
|
+
}
|
|
260
|
+
for (const flag of ["back", "live"]) {
|
|
261
|
+
if (o[flag] !== undefined && typeof o[flag] !== "boolean") {
|
|
262
|
+
push("BAD_VALUE", `${at}.${flag}`, o[flag], [],
|
|
263
|
+
["true", "false"]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (o.fade !== undefined && typeof o.fade !== "string") {
|
|
267
|
+
push("BAD_FIELD", `${at}.fade`, o.fade, [],
|
|
268
|
+
["a fade mode, as a string"]);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// A node is either one hop or a chain, and the runtime decides
|
|
273
|
+
// by looking at `chain` FIRST: `normalizeEdges` takes a
|
|
274
|
+
// non-empty chain and never reads node-level `from`/`to`. The
|
|
275
|
+
// check mirrors that order, so what passes here is what runs.
|
|
276
|
+
const chained = Array.isArray(node.chain) && node.chain.length > 0;
|
|
277
|
+
|
|
278
|
+
if (node.chain !== undefined && !Array.isArray(node.chain)) {
|
|
279
|
+
push("BAD_FIELD", `${path}.chain`, node.chain, [],
|
|
280
|
+
["an array of edges: [{ from, to }, ...]"]);
|
|
281
|
+
} else if (Array.isArray(node.chain) && !node.chain.length) {
|
|
282
|
+
// The runtime does not reject this — it falls back to the
|
|
283
|
+
// single-hop fields — but it is never what was meant.
|
|
284
|
+
push("BAD_VALUE", `${path}.chain`, node.chain, [],
|
|
285
|
+
["at least one edge, or no `chain` key at all"]);
|
|
149
286
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
287
|
+
|
|
288
|
+
checkOpts(node, path);
|
|
289
|
+
|
|
290
|
+
if (chained) {
|
|
291
|
+
// `from`/`to` written BESIDE a chain are dead, not merged.
|
|
292
|
+
// Worth reporting precisely because the page still works,
|
|
293
|
+
// using the chain, so the ignored pair looks as though it
|
|
294
|
+
// took effect.
|
|
295
|
+
for (const key of ["from", "to"]) {
|
|
296
|
+
if (node[key] !== undefined) {
|
|
297
|
+
push("IGNORED_FIELD", `${path}.${key}`, node[key], [],
|
|
298
|
+
["remove it, or drop `chain` and keep the single hop"]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
node.chain.forEach((edge, j) => {
|
|
302
|
+
const at = `${path}.chain[${j}]`;
|
|
303
|
+
if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
|
|
304
|
+
push("BAD_FIELD", at, edge, [], ["{ from, to }"]);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
checkEnds(edge, at);
|
|
308
|
+
checkOpts(edge, at);
|
|
309
|
+
for (const key in edge) {
|
|
310
|
+
if (EDGE_FIELDS.includes(key)) continue;
|
|
311
|
+
push("UNKNOWN_PARAM", `${at}.${key}`, key,
|
|
312
|
+
suggest(key, EDGE_FIELDS), EDGE_FIELDS);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
checkEnds(node, path);
|
|
154
317
|
}
|
|
318
|
+
|
|
155
319
|
// Unknown keys, checked here for the same reason they are
|
|
156
320
|
// checked on a raster node: a morph silently ignores what it
|
|
157
321
|
// does not recognise, so `duraton: 900` produces a transition
|