nodality 1.3.5 → 1.3.7
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/dist/animator.cjs.js +1 -1
- package/dist/animator.cjs.js.LICENSE.txt +3 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +3 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js +1 -1
- package/dist/base.cjs.js.LICENSE.txt +3 -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 +3 -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 +3 -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 -1
- package/dist/button.cjs.js +1 -1
- package/dist/button.cjs.js.LICENSE.txt +3 -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 +3 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +3 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js +1 -1
- package/dist/code.cjs.js.LICENSE.txt +3 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js +1 -1
- package/dist/container.cjs.js.LICENSE.txt +3 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js +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 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +1 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +3 -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 -1
- package/dist/flex-card.cjs.js +1 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +3 -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 +3 -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 +3 -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 +3 -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 +3 -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 +3 -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 +3 -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 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +1 -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 +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +3 -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 +3 -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 +3 -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 +3 -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 +3 -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 +3 -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 +3 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +3 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js +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 +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 +3 -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 +3 -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 +3 -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 +3 -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 +3 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +3 -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 +3 -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 +3 -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 +3 -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 +3 -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 +3 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +3 -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 +3 -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 +1 -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 +1 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +16 -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 +1 -1
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +98 -5
- package/lib/element-params.generated.js +1 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +1 -1
- package/lib/normalize-spec.js +1 -1
- package/lib/parse-html.js +1 -1
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +215 -5
- package/lib/raster-presets.js +1 -1
- package/lib/resolve-refs.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 +83 -2
- package/lib/webmcp-adapter.js +1 -1
- package/package.json +2 -2
- package/schema.json +609 -3264
- package/scripts/generate-schema.mjs +102 -12
- package/skills/nodality/SKILL.md +20 -2
- /package/bin/{nodality.js → nodality.mjs} +0 -0
package/lib/element-mapper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.7
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -125,9 +125,35 @@ const navEntry = (i) =>
|
|
|
125
125
|
// The failure this guards is silent: a composite handed content in the slot it
|
|
126
126
|
// does not read emits its placeholder template, so the page renders, nothing
|
|
127
127
|
// throws, and the content is simply gone.
|
|
128
|
+
// Kept byte-equal to `CONTENT_SLOT` in lib/validate-nodes.js, which cannot be
|
|
129
|
+
// imported here: this file pulls in every layout component and therefore a DOM,
|
|
130
|
+
// and the validator has to run in a bare Node process. Two copies is the price
|
|
131
|
+
// of that, and they had already drifted -- `stack` was declared here and not
|
|
132
|
+
// there -- so a unit test now pins them equal rather than trusting the comment.
|
|
133
|
+
//
|
|
134
|
+
// The per-type descriptions below are read by scripts/generate-schema.mjs. They
|
|
135
|
+
// live here because the mapper is what decides how a slot is read; before they
|
|
136
|
+
// existed the generator keyed descriptions by parameter NAME across every file
|
|
137
|
+
// it scanned, first writer winning, so the picker's `items` documentation was
|
|
138
|
+
// served for `cards`, `nav`, `sideNav`, `table` and `ulist` as well.
|
|
139
|
+
// Names whose meaning is decided by the element type, not by whichever file
|
|
140
|
+
// happened to document them first. See the SCOPED note in generate-schema.mjs.
|
|
141
|
+
//@scoped items
|
|
142
|
+
//@scoped type
|
|
143
|
+
//@scoped id
|
|
144
|
+
//@ type: The element type, which selects the mapper that builds it -- "h1", "cards", "form", and so on.
|
|
145
|
+
//@ id: This element's DOM id. Also what a design node's `target` names, and what `parse` recovers an element by.
|
|
146
|
+
//@ cards.items: The cards. Each entry is either a data object -- { title, link, img } -- or an ARRAY of element specs, which renders as one card containing them.
|
|
147
|
+
//@ nav.items: The links, each `{ title, link }` -- or a plain string.
|
|
148
|
+
//@ sideNav.items: The links, each `{ title, link }` -- or a plain string.
|
|
149
|
+
//@ table.items: The rows. Each entry is a `{ title, link }` or `{ text, url }` data object, a `[value, text]` pair, or an element spec.
|
|
150
|
+
//@ ulist.items: The list entries, each an element spec -- `{ type, ... }` -- or a plain string.
|
|
151
|
+
//@ dropdown.items: The choices, each a `[value, text]` pair -- or a plain string used as both.
|
|
152
|
+
//@ radio.items: The choices, each a plain string.
|
|
128
153
|
const CONTENT_SLOT = Object.freeze({
|
|
129
154
|
cards: "items", table: "items", ulist: "items",
|
|
130
155
|
nav: "items", sideNav: "items",
|
|
156
|
+
dropdown: "items", picker: "items", radio: "items",
|
|
131
157
|
row: "children", form: "children", stack: "children", wrap: "children",
|
|
132
158
|
});
|
|
133
159
|
|
|
@@ -295,6 +321,17 @@ class ElementMapper { // 22:09:58 04/11/2024
|
|
|
295
321
|
// return new Text("Hello").set({size: "S3"});
|
|
296
322
|
|
|
297
323
|
return new FlexRow().set({
|
|
324
|
+
// Everything the author declared. Without it a row's
|
|
325
|
+
// options were the node lookups alone, every one of
|
|
326
|
+
// them `undefined` when no node targets this element,
|
|
327
|
+
// so FlexRow.toCode() -- which correctly strips
|
|
328
|
+
// undefined -- emitted `.set({})` and the emitted
|
|
329
|
+
// source carried nothing. The empty object was the
|
|
330
|
+
// symptom; this is the cause.
|
|
331
|
+
...this.elOpts(el, [
|
|
332
|
+
"stroke", "gradient", "animation", "shadow", "span",
|
|
333
|
+
"backgroundOp", "marginOp", "transform", "filtera", "raster",
|
|
334
|
+
]),
|
|
298
335
|
stroke: this.filtero("blast", el.id, obj.customOptions),
|
|
299
336
|
gradient: this.filtero("gradient", el.id, obj.customOptions),
|
|
300
337
|
animation: this.filtero("animation", el.id, obj.customOptions),
|
|
@@ -1624,6 +1661,27 @@ let elo = new Polygon({ id: "hex" })
|
|
|
1624
1661
|
|
|
1625
1662
|
return new Text(el.text || el.value)
|
|
1626
1663
|
.set({
|
|
1664
|
+
// Everything the author declared, first, so that every option
|
|
1665
|
+
// Text reads is reachable from a spec. Hand-listing the
|
|
1666
|
+
// forwards left 36 of this type's 56 documented options
|
|
1667
|
+
// settable only on the emitted component -- `background`,
|
|
1668
|
+
// `align`, `width`, `opacity`, `radius` among them -- so a
|
|
1669
|
+
// page could declare one, validate clean, and render without
|
|
1670
|
+
// it. The note on `tag` below was one instance of that gap,
|
|
1671
|
+
// closed one option at a time; this closes the rest.
|
|
1672
|
+
//
|
|
1673
|
+
// The spread comes FIRST and every assignment below overrides
|
|
1674
|
+
// it, so nothing the mapper computes can be displaced by a
|
|
1675
|
+
// field of the same name. The drop list is the keys this
|
|
1676
|
+
// method derives from the NODE array: they belong to N, they
|
|
1677
|
+
// are resolved by `filtero` below, and letting E supply them
|
|
1678
|
+
// would both cross the two halves of the format and let an
|
|
1679
|
+
// empty lookup overwrite an author's value with `undefined`.
|
|
1680
|
+
...this.elOpts(el, [
|
|
1681
|
+
"value", // constructor argument, above
|
|
1682
|
+
"stroke", "gradient", "animation", "shadow", "span",
|
|
1683
|
+
"backgroundOp", "marginOp", "transform", "filtera", "raster",
|
|
1684
|
+
]),
|
|
1627
1685
|
id: el.id,
|
|
1628
1686
|
class: el.class,
|
|
1629
1687
|
color: el.color,
|
|
@@ -1662,7 +1720,14 @@ let elo = new Polygon({ id: "hex" })
|
|
|
1662
1720
|
|
|
1663
1721
|
return new Image(el.url)
|
|
1664
1722
|
.set({
|
|
1665
|
-
|
|
1723
|
+
// elOpts rather than a raw `...el`: the raw spread put `type`,
|
|
1724
|
+
// `children` and `items` into the component's OPTIONS, where
|
|
1725
|
+
// they mean nothing, and -- now that toCode() serialises
|
|
1726
|
+
// options -- writes them into the emitted source as well. The
|
|
1727
|
+
// artefact is meant to be code a developer reads and owns, so
|
|
1728
|
+
// it should not carry the specification's own bookkeeping.
|
|
1729
|
+
...this.elOpts(el),
|
|
1730
|
+
url: el.url, // skipped by elOpts; the raw spread carried it
|
|
1666
1731
|
isFull:el.isFull,
|
|
1667
1732
|
// index: obj.i + "",
|
|
1668
1733
|
animation: this.filtero("animation", el.id, obj.customOptions),
|
|
@@ -1857,12 +1922,28 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1857
1922
|
|
|
1858
1923
|
|
|
1859
1924
|
|
|
1925
|
+
// The author's own options, serialised into the emitted source. This
|
|
1926
|
+
// mapper returns SOURCE rather than an instance, so there is nothing to
|
|
1927
|
+
// spread onto -- the equivalent of `...elOpts(el)` here is to write the
|
|
1928
|
+
// entries into the `.set({...})` literal being generated.
|
|
1929
|
+
//
|
|
1930
|
+
// They come AFTER the four template defaults and BEFORE the node ops, and
|
|
1931
|
+
// that order is the contract: a default is a starting point an author may
|
|
1932
|
+
// overrule, so `{type:"cards", gap:"2rem"}` wins over `gap:"1rem"`; a node
|
|
1933
|
+
// is the N half of the format addressing this element by id, so it wins
|
|
1934
|
+
// over E. Before this, every one of these keys was accepted by the
|
|
1935
|
+
// validator, advertised by the schema, and silently dropped on the floor.
|
|
1936
|
+
const authored = Object.entries(this.elOpts(obj.el, [
|
|
1937
|
+
"backgroundCard", // consumed above, to pick the template
|
|
1938
|
+
"gradient", "animation", "shadow", "marginOp", "filtera", "blast", "raster",
|
|
1939
|
+
])).map(([k, v]) => `,\n ${JSON.stringify(k)}: ${JSON.stringify(v)}`).join("");
|
|
1940
|
+
|
|
1860
1941
|
return ` new FlexGrid()
|
|
1861
1942
|
.set({
|
|
1862
1943
|
colat: "700px",
|
|
1863
1944
|
wrap: true,
|
|
1864
1945
|
align: "center",
|
|
1865
|
-
gap: "1rem"
|
|
1946
|
+
gap: "1rem"${authored}
|
|
1866
1947
|
${
|
|
1867
1948
|
gradient !== undefined ? `,\n gradient: ${JSON.stringify(gradient)}` : ""
|
|
1868
1949
|
}${
|
|
@@ -2118,7 +2199,13 @@ static form(obj){
|
|
|
2118
2199
|
// slice on a word. A caller-supplied `size` still passes through.
|
|
2119
2200
|
const wrapper = new Wrapper(kind)
|
|
2120
2201
|
.set({
|
|
2121
|
-
|
|
2202
|
+
// elOpts rather than a raw `...el`: the raw spread put `type`,
|
|
2203
|
+
// `children` and `items` into the component's OPTIONS, where
|
|
2204
|
+
// they mean nothing, and -- now that toCode() serialises
|
|
2205
|
+
// options -- writes them into the emitted source as well. The
|
|
2206
|
+
// artefact is meant to be code a developer reads and owns, so
|
|
2207
|
+
// it should not carry the specification's own bookkeeping.
|
|
2208
|
+
...this.elOpts(el),
|
|
2122
2209
|
font: el.font ?? "Arial",
|
|
2123
2210
|
stroke: this.filtero("blast", el.id, obj.customOptions),
|
|
2124
2211
|
gradient: this.filtero("gradient", el.id, obj.customOptions),
|
|
@@ -2158,7 +2245,13 @@ static form(obj){
|
|
|
2158
2245
|
// output of getElType on a non-heading type.
|
|
2159
2246
|
const list = new UList(kind)
|
|
2160
2247
|
.set({
|
|
2161
|
-
|
|
2248
|
+
// elOpts rather than a raw `...el`: the raw spread put `type`,
|
|
2249
|
+
// `children` and `items` into the component's OPTIONS, where
|
|
2250
|
+
// they mean nothing, and -- now that toCode() serialises
|
|
2251
|
+
// options -- writes them into the emitted source as well. The
|
|
2252
|
+
// artefact is meant to be code a developer reads and owns, so
|
|
2253
|
+
// it should not carry the specification's own bookkeeping.
|
|
2254
|
+
...this.elOpts(el),
|
|
2162
2255
|
font: el.font ?? "Arial",
|
|
2163
2256
|
stroke: this.filtero("blast", el.id, obj.customOptions),
|
|
2164
2257
|
gradient: this.filtero("gradient", el.id, obj.customOptions),
|
package/lib/link-getter.js
CHANGED
package/lib/morph-node.js
CHANGED
package/lib/normalize-spec.js
CHANGED
package/lib/parse-html.js
CHANGED
package/lib/raster-inspect.js
CHANGED
package/lib/raster-ops.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.7
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -2526,6 +2526,11 @@ ${defaultSeam}
|
|
|
2526
2526
|
// computes, on a clone, so the snapshot matches the live layout.
|
|
2527
2527
|
const VIEWPORT_UNIT = /\d(?:vw|vh|vmin|vmax|dvw|dvh|svw|svh|lvw|lvh)\b/i;
|
|
2528
2528
|
|
|
2529
|
+
// Text metrics that must reach the snapshot as pixels. Anything relative here
|
|
2530
|
+
// gives the foreignObject a chance to resolve it against its own box instead of
|
|
2531
|
+
// the page — see part 1b of freezeStyles.
|
|
2532
|
+
const FROZEN_LENGTHS = ["font-size", "line-height", "letter-spacing", "word-spacing"];
|
|
2533
|
+
|
|
2529
2534
|
// Inheritable text properties. A foreignObject rendered from a data: URI
|
|
2530
2535
|
// carries NO page stylesheet, and a clone carries only inline styles — so
|
|
2531
2536
|
// anything a rule or an ancestor supplied is gone. Nodality's own
|
|
@@ -2570,8 +2575,8 @@ function freezeStyles(original, clone) {
|
|
|
2570
2575
|
// 5vw)` renders at a different size than it does on the page.
|
|
2571
2576
|
// The glyphs then sit somewhere the DOM text does not, which
|
|
2572
2577
|
// is what makes a selection highlight look offset.
|
|
2578
|
+
let computed = null;
|
|
2573
2579
|
if (inline.length > 0) {
|
|
2574
|
-
let computed = null;
|
|
2575
2580
|
// Iterate a snapshot of the names: writing to style mutates the list.
|
|
2576
2581
|
for (const prop of Array.from(inline)) {
|
|
2577
2582
|
if (!VIEWPORT_UNIT.test(inline.getPropertyValue(prop))) continue;
|
|
@@ -2581,6 +2586,28 @@ function freezeStyles(original, clone) {
|
|
|
2581
2586
|
}
|
|
2582
2587
|
}
|
|
2583
2588
|
|
|
2589
|
+
// 1b. Text lengths are frozen to pixels ALWAYS, whatever unit they were
|
|
2590
|
+
// written in and whether or not they were set inline.
|
|
2591
|
+
//
|
|
2592
|
+
// The sweep above only rewrites a declaration whose own inline text
|
|
2593
|
+
// matches VIEWPORT_UNIT. That misses every other way a length can
|
|
2594
|
+
// resolve differently inside the foreignObject: `rem` against a root
|
|
2595
|
+
// that is not the page's root, `em` down an ancestor chain the clone
|
|
2596
|
+
// no longer has, and — the case that prompted this — a `clamp()` whose
|
|
2597
|
+
// chosen branch depends on a viewport the SVG does not share. A
|
|
2598
|
+
// headline written `clamp(2.4rem, 4.6vw, 3.4rem)` came out at a
|
|
2599
|
+
// fraction of its size on a phone while looking correct in desktop
|
|
2600
|
+
// device emulation, because the two resolve that clamp differently.
|
|
2601
|
+
//
|
|
2602
|
+
// Computed values are already absolute pixels, so this cannot change
|
|
2603
|
+
// what the page shows; it only removes the chance for the SVG to
|
|
2604
|
+
// recompute something the page had already decided.
|
|
2605
|
+
for (const prop of FROZEN_LENGTHS) {
|
|
2606
|
+
computed = computed || window.getComputedStyle(origs[i]);
|
|
2607
|
+
const px = computed.getPropertyValue(prop);
|
|
2608
|
+
if (px && px !== "normal") inline.setProperty(prop, px);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2584
2611
|
// 2. Inherited text styling, which the clone would otherwise lose.
|
|
2585
2612
|
const cs = window.getComputedStyle(origs[i]);
|
|
2586
2613
|
const parent = origs[i].parentElement;
|
|
@@ -2721,17 +2748,200 @@ function fitImageToBox(img, w, h, dpr) {
|
|
|
2721
2748
|
return c;
|
|
2722
2749
|
}
|
|
2723
2750
|
|
|
2751
|
+
// ── Web fonts inside the snapshot ────────────────────────────────────
|
|
2752
|
+
//
|
|
2753
|
+
// A foreignObject rendered from a data: URI fetches NO subresources. An
|
|
2754
|
+
// @font-face whose src is a URL therefore never loads inside the capture, and
|
|
2755
|
+
// the text draws in whatever family the SVG can resolve locally — the page's
|
|
2756
|
+
// own web font is simply not among them. Recapturing on document.fonts.ready
|
|
2757
|
+
// does not help: the font is ready on the PAGE, never inside the snapshot.
|
|
2758
|
+
//
|
|
2759
|
+
// The only way the real typeface reaches the capture is to carry the bytes in
|
|
2760
|
+
// with it, so the faces the subtree actually uses are fetched once, base64'd,
|
|
2761
|
+
// and emitted as an inline <style> ahead of the serialized markup.
|
|
2762
|
+
//
|
|
2763
|
+
// Everything here degrades to "no embedded font" rather than failing the
|
|
2764
|
+
// capture: a cross-origin stylesheet that refuses CORS, a font host that does
|
|
2765
|
+
// the same, an offline reload — each ends with the previous behaviour, which is
|
|
2766
|
+
// a fallback typeface, not a broken image.
|
|
2767
|
+
|
|
2768
|
+
/** Absolute font URL -> `data:` URI, or null when it could not be fetched. */
|
|
2769
|
+
const FONT_DATA_CACHE = new Map();
|
|
2770
|
+
|
|
2771
|
+
/** Parsed @font-face rules for the document, resolved once. */
|
|
2772
|
+
let FACE_RULES = null;
|
|
2773
|
+
|
|
2774
|
+
const FACE_BLOCK = /@font-face\s*{[^}]*}/gi;
|
|
2775
|
+
const FACE_FAMILY = /font-family\s*:\s*(['"]?)([^;'"]+)\1/i;
|
|
2776
|
+
const FACE_WEIGHT = /font-weight\s*:\s*([^;]+)/i;
|
|
2777
|
+
const FACE_STYLE = /font-style\s*:\s*([^;]+)/i;
|
|
2778
|
+
const FACE_SRC_URL = /url\((['"]?)([^)'"]+)\1\)/gi;
|
|
2779
|
+
const FACE_RANGE = /unicode-range\s*:\s*([^;}]+)/i;
|
|
2780
|
+
|
|
2781
|
+
/**
|
|
2782
|
+
* Does this face cover any character the capture actually contains?
|
|
2783
|
+
*
|
|
2784
|
+
* A provider splits one family into a subset per script — Google ships seven for
|
|
2785
|
+
* Inter — so embedding every subset for every weight in use put well over half a
|
|
2786
|
+
* megabyte of base64 into a single snapshot. Only the subsets the text actually
|
|
2787
|
+
* lands in are worth carrying. A face that declares no range covers everything
|
|
2788
|
+
* and is always kept.
|
|
2789
|
+
*/
|
|
2790
|
+
function faceCoversText(range, text) {
|
|
2791
|
+
if (!range) return true;
|
|
2792
|
+
const spans = [];
|
|
2793
|
+
for (const part of range.split(",")) {
|
|
2794
|
+
const m = /U\+([0-9a-fA-F?]+)(?:-([0-9a-fA-F]+))?/.exec(part.trim());
|
|
2795
|
+
if (!m) continue;
|
|
2796
|
+
if (m[1].indexOf("?") !== -1) {
|
|
2797
|
+
spans.push([parseInt(m[1].replace(/\?/g, "0"), 16),
|
|
2798
|
+
parseInt(m[1].replace(/\?/g, "F"), 16)]);
|
|
2799
|
+
} else {
|
|
2800
|
+
const lo = parseInt(m[1], 16);
|
|
2801
|
+
spans.push([lo, m[2] ? parseInt(m[2], 16) : lo]);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (!spans.length) return true;
|
|
2805
|
+
for (const ch of text) {
|
|
2806
|
+
const cp = ch.codePointAt(0);
|
|
2807
|
+
for (let i = 0; i < spans.length; i++) {
|
|
2808
|
+
if (cp >= spans[i][0] && cp <= spans[i][1]) return true;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function parseFaces(cssText, baseUrl) {
|
|
2815
|
+
const out = [];
|
|
2816
|
+
for (const block of cssText.match(FACE_BLOCK) || []) {
|
|
2817
|
+
const fam = FACE_FAMILY.exec(block);
|
|
2818
|
+
if (!fam) continue;
|
|
2819
|
+
const urls = [];
|
|
2820
|
+
let m;
|
|
2821
|
+
FACE_SRC_URL.lastIndex = 0;
|
|
2822
|
+
while ((m = FACE_SRC_URL.exec(block))) {
|
|
2823
|
+
try { urls.push(new URL(m[2], baseUrl).href); } catch (e) { /* skip */ }
|
|
2824
|
+
}
|
|
2825
|
+
if (!urls.length) continue;
|
|
2826
|
+
out.push({
|
|
2827
|
+
family: fam[2].trim().toLowerCase(),
|
|
2828
|
+
weight: (FACE_WEIGHT.exec(block) || [, "400"])[1].trim(),
|
|
2829
|
+
style: (FACE_STYLE.exec(block) || [, "normal"])[1].trim(),
|
|
2830
|
+
range: (FACE_RANGE.exec(block) || [, ""])[1].trim(),
|
|
2831
|
+
block,
|
|
2832
|
+
urls,
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
return out;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
async function collectFaceRules() {
|
|
2839
|
+
if (FACE_RULES) return FACE_RULES;
|
|
2840
|
+
const faces = [];
|
|
2841
|
+
for (const sheet of Array.from(document.styleSheets || [])) {
|
|
2842
|
+
// Same-origin sheets can be read directly. A cross-origin one throws on
|
|
2843
|
+
// cssRules, so it is fetched instead — Google Fonts and the like serve
|
|
2844
|
+
// their CSS with permissive CORS, and anything that does not simply
|
|
2845
|
+
// contributes no faces.
|
|
2846
|
+
let rules = null;
|
|
2847
|
+
try { rules = sheet.cssRules; } catch (e) { rules = null; }
|
|
2848
|
+
if (rules) {
|
|
2849
|
+
for (const rule of Array.from(rules)) {
|
|
2850
|
+
if (rule.constructor && rule.constructor.name === "CSSFontFaceRule") {
|
|
2851
|
+
faces.push(...parseFaces(rule.cssText, sheet.href || location.href));
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
} else if (sheet.href) {
|
|
2855
|
+
try {
|
|
2856
|
+
const res = await fetch(sheet.href, { mode: "cors" });
|
|
2857
|
+
if (res.ok) faces.push(...parseFaces(await res.text(), sheet.href));
|
|
2858
|
+
} catch (e) { /* no faces from this sheet */ }
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
FACE_RULES = faces;
|
|
2862
|
+
return faces;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
async function asDataUri(url) {
|
|
2866
|
+
if (FONT_DATA_CACHE.has(url)) return FONT_DATA_CACHE.get(url);
|
|
2867
|
+
let uri = null;
|
|
2868
|
+
try {
|
|
2869
|
+
const res = await fetch(url, { mode: "cors" });
|
|
2870
|
+
if (res.ok) {
|
|
2871
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
2872
|
+
let bin = "";
|
|
2873
|
+
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]);
|
|
2874
|
+
const type = url.endsWith(".woff2") ? "font/woff2"
|
|
2875
|
+
: url.endsWith(".woff") ? "font/woff" : "font/ttf";
|
|
2876
|
+
uri = `data:${type};base64,${btoa(bin)}`;
|
|
2877
|
+
}
|
|
2878
|
+
} catch (e) { uri = null; }
|
|
2879
|
+
FONT_DATA_CACHE.set(url, uri);
|
|
2880
|
+
return uri;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
/** The (family, weight, style) triples this subtree actually renders in. */
|
|
2884
|
+
function usedFaces(el) {
|
|
2885
|
+
const seen = new Map();
|
|
2886
|
+
for (const node of [el, ...el.querySelectorAll("*")]) {
|
|
2887
|
+
const cs = window.getComputedStyle(node);
|
|
2888
|
+
const first = (cs.fontFamily || "").split(",")[0].trim().replace(/^['"]|['"]$/g, "");
|
|
2889
|
+
if (!first) continue;
|
|
2890
|
+
const key = `${first.toLowerCase()}|${cs.fontWeight}|${cs.fontStyle}`;
|
|
2891
|
+
if (!seen.has(key)) {
|
|
2892
|
+
seen.set(key, { family: first.toLowerCase(), weight: cs.fontWeight, style: cs.fontStyle });
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
return Array.from(seen.values());
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
async function embeddedFontCss(el) {
|
|
2899
|
+
let faces;
|
|
2900
|
+
try { faces = await collectFaceRules(); } catch (e) { return ""; }
|
|
2901
|
+
if (!faces.length) return "";
|
|
2902
|
+
|
|
2903
|
+
const wanted = usedFaces(el);
|
|
2904
|
+
const text = el.textContent || "";
|
|
2905
|
+
const chosen = faces.filter((f) => faceCoversText(f.range, text) && wanted.some((u) =>
|
|
2906
|
+
u.family === f.family &&
|
|
2907
|
+
(f.style === u.style || f.style === "normal") &&
|
|
2908
|
+
// Google emits one face per weight; a variable face states a range and
|
|
2909
|
+
// is kept for every weight that asks for the family.
|
|
2910
|
+
(f.weight === String(u.weight) || /\s/.test(f.weight))));
|
|
2911
|
+
if (!chosen.length) return "";
|
|
2912
|
+
|
|
2913
|
+
const parts = [];
|
|
2914
|
+
for (const face of chosen) {
|
|
2915
|
+
// First url that actually comes back — the list is a preference order.
|
|
2916
|
+
let uri = null;
|
|
2917
|
+
for (const u of face.urls) {
|
|
2918
|
+
uri = await asDataUri(u);
|
|
2919
|
+
if (uri) break;
|
|
2920
|
+
}
|
|
2921
|
+
if (!uri) continue;
|
|
2922
|
+
parts.push(
|
|
2923
|
+
`@font-face{font-family:'${face.family}';font-style:${face.style};` +
|
|
2924
|
+
`font-weight:${face.weight};src:url(${uri});}`);
|
|
2925
|
+
}
|
|
2926
|
+
return parts.join("");
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2724
2929
|
function snapshotToImage(el, w, h, dpr) {
|
|
2725
2930
|
const direct = soleImageOf(el);
|
|
2726
2931
|
if (direct) return Promise.resolve(fitImageToBox(direct, w, h, dpr));
|
|
2727
2932
|
|
|
2728
|
-
|
|
2933
|
+
// The font CSS is resolved before serialising. It is cached after the first
|
|
2934
|
+
// capture, so only the very first snapshot on a page pays for the fetch, and
|
|
2935
|
+
// a failure returns "" and leaves the previous fallback behaviour intact.
|
|
2936
|
+
return embeddedFontCss(el).catch(() => "").then((fontCss) => new Promise((resolve, reject) => {
|
|
2729
2937
|
const serialized = new XMLSerializer()
|
|
2730
2938
|
.serializeToString(freezeStyles(el, el.cloneNode(true)));
|
|
2731
2939
|
const svg =
|
|
2732
2940
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${w * dpr}" height="${h * dpr}" viewBox="0 0 ${w} ${h}">` +
|
|
2733
2941
|
`<foreignObject width="100%" height="100%">` +
|
|
2734
|
-
`<div xmlns="http://www.w3.org/1999/xhtml"
|
|
2942
|
+
`<div xmlns="http://www.w3.org/1999/xhtml">` +
|
|
2943
|
+
(fontCss ? `<style>${fontCss}</style>` : "") +
|
|
2944
|
+
`${serialized}</div>` +
|
|
2735
2945
|
`</foreignObject></svg>`;
|
|
2736
2946
|
// createElement, NOT `new Image()` — this library exports a
|
|
2737
2947
|
// component named `Image`, and consumers publish the exports onto
|
|
@@ -2741,7 +2951,7 @@ function snapshotToImage(el, w, h, dpr) {
|
|
|
2741
2951
|
img.onload = () => resolve(img);
|
|
2742
2952
|
img.onerror = reject;
|
|
2743
2953
|
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
2744
|
-
});
|
|
2954
|
+
}));
|
|
2745
2955
|
}
|
|
2746
2956
|
|
|
2747
2957
|
// ── switch: pick a sub-chain by condition ────────────────────────────
|
package/lib/raster-presets.js
CHANGED
package/lib/resolve-refs.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