@jini-ai/cms 0.5.2 → 0.5.3
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/CHANGELOG.md +11 -0
- package/README.md +14 -8
- package/dist/comments/errors.d.ts +23 -0
- package/dist/comments/errors.js +25 -0
- package/dist/comments/express/index.d.ts +2 -0
- package/dist/comments/express/index.js +2 -0
- package/dist/comments/express/submit-handler.d.ts +30 -0
- package/dist/comments/express/submit-handler.js +85 -0
- package/dist/comments/hooks.d.ts +39 -0
- package/dist/comments/hooks.js +41 -0
- package/dist/comments/index.d.ts +11 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/ingress.d.ts +57 -0
- package/dist/comments/ingress.js +173 -0
- package/dist/comments/module.d.ts +94 -0
- package/dist/comments/module.js +73 -0
- package/dist/comments/ports.d.ts +184 -0
- package/dist/comments/ports.js +2 -0
- package/dist/comments/repo.memory.d.ts +70 -0
- package/dist/comments/repo.memory.js +120 -0
- package/dist/comments/sanitize.d.ts +15 -0
- package/dist/comments/sanitize.js +31 -0
- package/dist/comments/spam.heuristic.d.ts +8 -0
- package/dist/comments/spam.heuristic.js +54 -0
- package/dist/comments/sql/index.d.ts +3 -0
- package/dist/comments/sql/index.js +3 -0
- package/dist/comments/sql/repo.d.ts +77 -0
- package/dist/comments/sql/repo.js +190 -0
- package/dist/comments/sql/repo.rows.d.ts +49 -0
- package/dist/comments/sql/repo.rows.js +71 -0
- package/dist/comments/types.d.ts +187 -0
- package/dist/comments/types.js +39 -0
- package/dist/comments/write-service.d.ts +121 -0
- package/dist/comments/write-service.js +144 -0
- package/dist/core/commands/command.d.ts +2 -9
- package/dist/core/commands/command.js +2 -9
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/core/keyset-cursor.d.ts +25 -0
- package/dist/core/keyset-cursor.js +20 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/ports.js +0 -1
- package/dist/core/tools/args.d.ts +1 -46
- package/dist/core/tools/args.js +1 -44
- package/dist/media/index.d.ts +0 -3
- package/dist/media/index.js +1 -4
- package/dist/media/node.d.ts +5 -0
- package/dist/media/node.js +6 -0
- package/dist/redirects/capture.d.ts +80 -0
- package/dist/redirects/capture.js +66 -0
- package/dist/redirects/express/index.d.ts +27 -0
- package/dist/redirects/express/index.js +40 -0
- package/dist/redirects/hit-sink.d.ts +56 -0
- package/dist/redirects/hit-sink.js +44 -0
- package/dist/redirects/host-config/index.d.ts +40 -0
- package/dist/redirects/host-config/index.js +145 -0
- package/dist/redirects/index.d.ts +14 -0
- package/dist/redirects/index.js +14 -0
- package/dist/redirects/matcher.d.ts +58 -0
- package/dist/redirects/matcher.js +144 -0
- package/dist/redirects/order.d.ts +30 -0
- package/dist/redirects/order.js +26 -0
- package/dist/redirects/ports.d.ts +242 -0
- package/dist/redirects/ports.internal.d.ts +57 -0
- package/dist/redirects/ports.internal.js +14 -0
- package/dist/redirects/ports.js +2 -0
- package/dist/redirects/redirects.d.ts +226 -0
- package/dist/redirects/redirects.js +590 -0
- package/dist/redirects/referrer-alias.d.ts +31 -0
- package/dist/redirects/referrer-alias.js +33 -0
- package/dist/redirects/repo.memory.d.ts +51 -0
- package/dist/redirects/repo.memory.js +98 -0
- package/dist/redirects/reserved-destination.d.ts +58 -0
- package/dist/redirects/reserved-destination.js +59 -0
- package/dist/redirects/resolver.d.ts +100 -0
- package/dist/redirects/resolver.js +87 -0
- package/dist/redirects/spa/index.d.ts +30 -0
- package/dist/redirects/spa/index.js +27 -0
- package/dist/redirects/sql/index.d.ts +4 -0
- package/dist/redirects/sql/index.js +4 -0
- package/dist/redirects/sql/repo.d.ts +82 -0
- package/dist/redirects/sql/repo.js +143 -0
- package/dist/redirects/sql/repo.rows.d.ts +49 -0
- package/dist/redirects/sql/repo.rows.js +79 -0
- package/dist/redirects/sql/row-types.d.ts +34 -0
- package/dist/redirects/sql/row-types.js +2 -0
- package/dist/redirects/target-gate.d.ts +30 -0
- package/dist/redirects/target-gate.js +85 -0
- package/dist/redirects/types.d.ts +213 -0
- package/dist/redirects/types.js +42 -0
- package/dist/seo/absolute-url.d.ts +58 -0
- package/dist/seo/absolute-url.js +117 -0
- package/dist/seo/dom/index.d.ts +14 -0
- package/dist/seo/dom/index.js +52 -0
- package/dist/seo/errors.d.ts +35 -0
- package/dist/seo/errors.js +35 -0
- package/dist/seo/feed.d.ts +49 -0
- package/dist/seo/feed.js +61 -0
- package/dist/seo/index.d.ts +13 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/media.d.ts +57 -0
- package/dist/seo/media.js +134 -0
- package/dist/seo/page-head.d.ts +18 -0
- package/dist/seo/page-head.js +114 -0
- package/dist/seo/ports.d.ts +221 -0
- package/dist/seo/ports.js +27 -0
- package/dist/seo/seo.d.ts +73 -0
- package/dist/seo/seo.js +230 -0
- package/dist/seo/sitemap.d.ts +139 -0
- package/dist/seo/sitemap.js +217 -0
- package/dist/seo/types.d.ts +278 -0
- package/dist/seo/types.js +2 -0
- package/dist/seo/write-service.d.ts +79 -0
- package/dist/seo/write-service.js +273 -0
- package/dist/seo/xml-escape.d.ts +16 -0
- package/dist/seo/xml-escape.js +19 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +5 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +3 -38
- package/dist/settings/index.js +3 -35
- package/dist/taxonomy/write-service.d.ts +2 -30
- package/dist/widgets/authorize-helper.d.ts +42 -0
- package/dist/widgets/authorize-helper.js +16 -0
- package/dist/widgets/concurrency.d.ts +12 -0
- package/dist/widgets/concurrency.js +44 -0
- package/dist/widgets/config-validation.d.ts +34 -0
- package/dist/widgets/config-validation.js +125 -0
- package/dist/widgets/embed-service.d.ts +133 -0
- package/dist/widgets/embed-service.js +460 -0
- package/dist/widgets/embed-validation.d.ts +23 -0
- package/dist/widgets/embed-validation.js +38 -0
- package/dist/widgets/entry-payload.d.ts +103 -0
- package/dist/widgets/entry-payload.js +160 -0
- package/dist/widgets/errors.d.ts +136 -0
- package/dist/widgets/errors.js +136 -0
- package/dist/widgets/host-ports.d.ts +149 -0
- package/dist/widgets/host-ports.js +2 -0
- package/dist/widgets/html/html-embeds.d.ts +247 -0
- package/dist/widgets/html/html-embeds.js +408 -0
- package/dist/widgets/html/index.d.ts +5 -0
- package/dist/widgets/html/index.js +4 -0
- package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
- package/dist/widgets/html/legacy-widget-markers.js +45 -0
- package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
- package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +19 -0
- package/dist/widgets/markers/html-attributes.d.ts +98 -0
- package/dist/widgets/markers/html-attributes.js +216 -0
- package/dist/widgets/markers/index.d.ts +3 -0
- package/dist/widgets/markers/index.js +3 -0
- package/dist/widgets/markers/marker.d.ts +496 -0
- package/dist/widgets/markers/marker.js +780 -0
- package/dist/widgets/page-embed-types.d.ts +42 -0
- package/dist/widgets/page-embed-types.js +42 -0
- package/dist/widgets/ports.d.ts +142 -0
- package/dist/widgets/ports.js +2 -0
- package/dist/widgets/read-service.d.ts +81 -0
- package/dist/widgets/read-service.js +79 -0
- package/dist/widgets/region-area-service.d.ts +110 -0
- package/dist/widgets/region-area-service.js +180 -0
- package/dist/widgets/registry.d.ts +217 -0
- package/dist/widgets/registry.js +172 -0
- package/dist/widgets/repo.memory.d.ts +41 -0
- package/dist/widgets/repo.memory.js +48 -0
- package/dist/widgets/resolver-service.d.ts +209 -0
- package/dist/widgets/resolver-service.js +909 -0
- package/dist/widgets/resolvers/contact-form.d.ts +26 -0
- package/dist/widgets/resolvers/contact-form.js +54 -0
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
- package/dist/widgets/resolvers/index.d.ts +65 -0
- package/dist/widgets/resolvers/index.js +155 -0
- package/dist/widgets/resolvers/menu.d.ts +42 -0
- package/dist/widgets/resolvers/menu.js +43 -0
- package/dist/widgets/resolvers/ports.d.ts +82 -0
- package/dist/widgets/resolvers/ports.js +2 -0
- package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
- package/dist/widgets/resolvers/recent-entries.js +165 -0
- package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
- package/dist/widgets/resolvers/taxonomy.js +34 -0
- package/dist/widgets/sql/index.d.ts +3 -0
- package/dist/widgets/sql/index.js +3 -0
- package/dist/widgets/sql/repo.d.ts +41 -0
- package/dist/widgets/sql/repo.js +82 -0
- package/dist/widgets/sql/tables.d.ts +13 -0
- package/dist/widgets/sql/tables.js +2 -0
- package/dist/widgets/types.d.ts +213 -0
- package/dist/widgets/types.js +17 -0
- package/dist/widgets/where-used.d.ts +41 -0
- package/dist/widgets/where-used.js +11 -0
- package/dist/widgets/write-service.d.ts +204 -0
- package/dist/widgets/write-service.js +332 -0
- package/dist/workspace/delete.d.ts +1 -1
- package/package.json +220 -11
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The embed-reference view of a `"html"`-format Page's body (SPEC-047 Slice 2, generalized
|
|
3
|
+
* 2026-08-07 per `project-tovu-generic-embed-contract`, rewired onto the shared marker parser
|
|
4
|
+
* 2026-08-10).
|
|
5
|
+
*
|
|
6
|
+
* Purpose:
|
|
7
|
+
* A TipTap `widgetEmbed` node has a structured home (`bodyJson`) to carry a `{placementId,
|
|
8
|
+
* widgetEntryId}` attrs object. A Page's body is a free-form HTML string with no such tree, so an
|
|
9
|
+
* embed there has to be a literal markup convention instead:
|
|
10
|
+
* `<div data-embed-config='{"type":"widget","id":"{widgetEntryId}"}'></div>` (or `"type":"media"`,
|
|
11
|
+
* `"post"`, or any future type token), anywhere in the page's `body_html`.
|
|
12
|
+
*
|
|
13
|
+
* **This file no longer owns a pattern.** Locating and parsing a marker is `core/embeds/marker.ts`'s
|
|
14
|
+
* single job; this module is only the adapter that projects a parsed marker into the
|
|
15
|
+
* {@link PageHtmlEmbedRef} shape `resolver-service.ts` resolves against. Before the 2026-08-10
|
|
16
|
+
* unification there were four regexes for one concept — this one, `static-render.ts`'s,
|
|
17
|
+
* `entry-refs/extractor.ts`'s deliberate second copy, and the theme-slot pair — and they had already
|
|
18
|
+
* drifted: this one required an EMPTY `<div></div>`, while the theme path deliberately matched a
|
|
19
|
+
* marker WITH authored fallback content inside it. Sharing the parser is what makes "what render.ts
|
|
20
|
+
* embeds" and "what entry_refs indexes" incapable of disagreeing, rather than merely tested for
|
|
21
|
+
* agreement after the fact.
|
|
22
|
+
*
|
|
23
|
+
* **Why `type` is a free string, not a closed union.** A closed union would mean this file's own
|
|
24
|
+
* type signature has to change every time a new embed type is added — exactly the coupling the
|
|
25
|
+
* generic contract exists to remove. Adding a fourth type is a new resolver registration
|
|
26
|
+
* (`resolver-service.ts`'s `HTML_EMBED_RESOLVERS`), never a scanner change. Unknown types flow
|
|
27
|
+
* through untouched; "is this type known" is a resolution-time question, never a scan-time one.
|
|
28
|
+
*
|
|
29
|
+
* **Inner content is now allowed, and that is a widening.** The old empty-div requirement meant any
|
|
30
|
+
* content inside a placeholder kept it from matching at all — it rendered verbatim and inert. The
|
|
31
|
+
* shared parser captures inner content instead, because a theme marker's authored content is a real
|
|
32
|
+
* fallback that must survive when nothing resolves. For most types the consequence is that
|
|
33
|
+
* {@link substituteHtmlEmbeds} replaces the WHOLE element, fallback content included, exactly as it
|
|
34
|
+
* already replaced the whole empty div: an html Page's embed placeholder is scaffolding for the
|
|
35
|
+
* resolved widget, not a nav that must survive a failed lookup. Unresolved still degrades to the
|
|
36
|
+
* REQ-28 placeholder via the caller's own `resolve`, never to a crash and never to raw marker markup
|
|
37
|
+
* reaching a visitor.
|
|
38
|
+
*
|
|
39
|
+
* **`"media"`, `"post"`, and `"content"` are wrapper-preserving.** An author who wraps one of these
|
|
40
|
+
* markers in their own element — `<div style="max-width:600px"
|
|
41
|
+
* data-embed-config='{"type":"media","id":"..."}'></div>`, or `<main class="page-body"
|
|
42
|
+
* data-embed-config='{"type":"content","id":"..."}'></main>` — expects that element and its own
|
|
43
|
+
* attributes (style, class, id, aria-*) to survive; only the marker's CONTENT should become the
|
|
44
|
+
* resolved markup. See {@link WRAPPER_PRESERVING_EMBED_TYPES}'s own doc for the per-type reasoning,
|
|
45
|
+
* and {@link substituteHtmlEmbeds}'s own doc for the one tag-nesting exception `"media"` alone needs.
|
|
46
|
+
*
|
|
47
|
+
* **`"widget"` (and any other type this stage resolves) keeps its element ONLY if the author gave it
|
|
48
|
+
* attributes (2026-09-16, direct owner ask: "regular attributes that somebody puts on a
|
|
49
|
+
* data-embed-config should work on the rendered tag").** A BARE `<div
|
|
50
|
+
* data-embed-config='{"type":"widget","id":"w1"}'></div>` is still whole-element replaced,
|
|
51
|
+
* byte-identical to before — `"widget"`'s resolved root tag is not a small, closed set (see
|
|
52
|
+
* {@link WRAPPER_PRESERVING_EMBED_TYPES}'s own doc for why it still cannot unconditionally join the
|
|
53
|
+
* wrapper-preserving group), but an author who wrote `<div style="max-width:600px"
|
|
54
|
+
* data-embed-config='{"type":"widget","id":"w1"}'></div>` plainly wants that `style` to survive.
|
|
55
|
+
* {@link withElementKeptIfAttributed} is the shared primitive for this rule.
|
|
56
|
+
*
|
|
57
|
+
* **`"media"`'s own attributes (2026-09-16) are partitioned, not simply kept-or-dropped.** See
|
|
58
|
+
* {@link MEDIA_ELEMENT_ATTRIBUTE_NAMES} and {@link substituteHtmlEmbeds}'s own doc.
|
|
59
|
+
*
|
|
60
|
+
* **Why attribute VALUES are not re-validated here for injection safety.** A Page's `body_html` is
|
|
61
|
+
* already unescaped, trusted-ish markup by the time it reaches this module (see
|
|
62
|
+
* `update-html.ts`'s own disclosure) — nothing this file does widens that trust boundary. The one
|
|
63
|
+
* thing this module owns defensively is bounding how much work an adversarial/oversized page can
|
|
64
|
+
* demand at render/index time (`MAX_HTML_EMBEDS_PER_PAGE`) and never using a captured id for
|
|
65
|
+
* anything other than a map/repo lookup key (never re-emitted into HTML unescaped, never
|
|
66
|
+
* interpolated into a query) — see `resolver-service.ts`'s `resolveHtmlPageEmbeds` and
|
|
67
|
+
* `extractor.ts`'s html-ref collector, both of which only ever use the id that way.
|
|
68
|
+
*
|
|
69
|
+
* **`slug`, an author-memorable alternative to `id` (2026-08-31).** The owner's own complaint —
|
|
70
|
+
* `{"type":"widget","id":"29721c44-a811-444f-b7b5-e61a9918a3a9"}` is not something a person can
|
|
71
|
+
* type from memory — is real, and this is the fix: `{"type":"widget","slug":"contact-form"}`.
|
|
72
|
+
* `slug` is projected here exactly like `name`/`variant` already are (a raw, unvalidated config
|
|
73
|
+
* key; deciding whether it is USABLE is a resolver-side concern), but it is a genuinely different
|
|
74
|
+
* mechanism from the dormant `name` field above, not a rename of it. `name` was scaffolded
|
|
75
|
+
* (2026-08-05/07 generic-embed-contract design, `project-tovu-generic-embed-contract` in
|
|
76
|
+
* `ADS-memory/`) for a fuzzy, POSSIBLY-AMBIGUOUS match — "exactly one match resolves; zero or >1
|
|
77
|
+
* degrades to the placeholder, never a guess" — and no resolver in this codebase has ever
|
|
78
|
+
* implemented that ambiguity guard. `slug` needs no such guard: it targets a column
|
|
79
|
+
* (`entries.slug`) carrying a real database `UNIQUE(workspace_id, type, slug)` index, so "more than
|
|
80
|
+
* one match" cannot occur by construction, not merely by convention. Reusing `name`'s never-shipped,
|
|
81
|
+
* ambiguity-hedged semantics for a value that is unique by database constraint would be modeling a
|
|
82
|
+
* problem this feature does not have.
|
|
83
|
+
*
|
|
84
|
+
* **`id` stays authoritative.** When a marker carries both, `id` wins and `slug` is not even
|
|
85
|
+
* consulted — this is a strict widening of the pre-existing "the id resolves" rule (a marker with no
|
|
86
|
+
* `slug` key behaves byte-identically to before), not a new fallback CHAIN where a present-but-broken
|
|
87
|
+
* `id` triggers a `slug` retry; nothing in this codebase's history committed to that stronger contract
|
|
88
|
+
* and building it would add a second resolution path for no requirement in hand. See
|
|
89
|
+
* `resolver-service.ts`'s `resolveWidgetTypeEmbeds` for where `slug` is actually resolved — today
|
|
90
|
+
* that is `"widget"` only (`entries.slug` is unique per `(workspaceId, type)`; `posts.slug` is
|
|
91
|
+
* equally unique and could gain the identical treatment cheaply in a follow-up, but is intentionally
|
|
92
|
+
* left untouched by this change; `media` has no `slug` column at all and cannot support this without
|
|
93
|
+
* a schema migration).
|
|
94
|
+
*/
|
|
95
|
+
import { parseMarkerAttributes, scanEmbedMarkers, substituteMarkers, withElementKeptIfAttributed, withInnerContentAndAttributes, withInnerContentFinal, } from "../markers/marker.js";
|
|
96
|
+
import { normalizeLegacyWidgetMarkers } from "./legacy-widget-markers.js";
|
|
97
|
+
/**
|
|
98
|
+
* Resource bound (Programmer workflow step 5a4) mirroring `embed-service.ts`'s
|
|
99
|
+
* `DEFAULT_MAX_EMBEDS_PER_DOCUMENT` for the TipTap path — same policy, same value, applied here
|
|
100
|
+
* because an html Page has no equivalent write-time guardrail (`PagesHtmlDocumentStore.write()`
|
|
101
|
+
* accepts any string; `embed-validation.ts`'s `validateWidgetEmbedMutation` is never in this path).
|
|
102
|
+
* Without a cap, a page carrying thousands of embed placeholders would grow `entry_refs` and the
|
|
103
|
+
* render-time resolved-id set unboundedly. `scanHtmlEmbeds` truncates AT this many refs; occurrences
|
|
104
|
+
* beyond the cap simply resolve to the same REQ-28 placeholder every other unresolved reference
|
|
105
|
+
* does (see `substituteHtmlEmbeds`'s own doc for why it does not need a matching count check).
|
|
106
|
+
*
|
|
107
|
+
* **This is a render/index-time resource guard, NOT a write-time invariant.** Nothing stops
|
|
108
|
+
* `pages_write_html`/the admin HTML editor from storing a page with 5,000 embed placeholders in
|
|
109
|
+
* `body_html` — `PagesHtmlDocumentStore.write()` has no count check of its own, unlike
|
|
110
|
+
* `embed-validation.ts`'s `validateWidgetEmbedMutation` (REQ-20) on the TipTap path, which rejects
|
|
111
|
+
* an over-limit mutation before it is ever persisted. This constant only bounds how much of an
|
|
112
|
+
* already-stored oversized page gets resolved/indexed on any single render or reindex pass — it
|
|
113
|
+
* does not guarantee such a page can never exist. A write-time cap would need its own design
|
|
114
|
+
* decision (reject vs. truncate vs. warn the operator) that has not been made; do not read this
|
|
115
|
+
* constant as implying one already has been.
|
|
116
|
+
*/
|
|
117
|
+
export const MAX_HTML_EMBEDS_PER_PAGE = 50;
|
|
118
|
+
/** An id long enough to be any real UUID/ULID but short enough that a pathological value can't
|
|
119
|
+
* itself become a resource concern once carried into a `Set`/`Map` key or an `entry_refs` row. */
|
|
120
|
+
const MAX_EMBED_ID_LENGTH = 200;
|
|
121
|
+
/** One config key as a string, or `null` when absent or of any other JSON type. The resolvers'
|
|
122
|
+
* `null`-means-unusable branches predate JSON configs and stay correct unchanged: a non-string
|
|
123
|
+
* `"id": 7` is exactly as unusable as a missing one. */
|
|
124
|
+
function configString(config, key) {
|
|
125
|
+
const value = config[key];
|
|
126
|
+
return typeof value === "string" ? value : null;
|
|
127
|
+
}
|
|
128
|
+
/** `null` when `rawId` is absent, empty, or beyond {@link MAX_EMBED_ID_LENGTH} — a bound too long to
|
|
129
|
+
* safely carry into a `Map`/`Set` key or an `entry_refs` row (see {@link MAX_EMBED_ID_LENGTH}'s own
|
|
130
|
+
* doc), same sanity check the old `data-widget-embed`/`data-form-embed` convention applied. Also used
|
|
131
|
+
* for `slug` (via {@link normalizeEmbedSlug}) — same resource-cost reasoning applies to a slug string
|
|
132
|
+
* carried into a repo lookup key, even though `id` and `slug` name two independently-validated marker
|
|
133
|
+
* keys, not one value under two spellings. */
|
|
134
|
+
function normalizeEmbedId(rawId) {
|
|
135
|
+
if (rawId === null || rawId.length === 0 || rawId.length > MAX_EMBED_ID_LENGTH)
|
|
136
|
+
return null;
|
|
137
|
+
return rawId;
|
|
138
|
+
}
|
|
139
|
+
/** `null` when `rawSlug` is absent, empty, or beyond {@link MAX_EMBED_ID_LENGTH} — see
|
|
140
|
+
* {@link normalizeEmbedId}'s own doc for why this shares that bound via one small wrapper rather than
|
|
141
|
+
* a shared rename: `id`/`slug` are independently-validated keys, not one value under two names. */
|
|
142
|
+
function normalizeEmbedSlug(rawSlug) {
|
|
143
|
+
return normalizeEmbedId(rawSlug);
|
|
144
|
+
}
|
|
145
|
+
/** `true` unless `config[key]` is the JSON boolean literal `false` (2026-09-04, `header`'s own
|
|
146
|
+
* doc on {@link PageHtmlEmbedRef}). Deliberately NOT `configString`'s "wrong type -> unusable"
|
|
147
|
+
* pattern: this is an opt-OUT, so the safe default for anything that isn't a recognized `false` —
|
|
148
|
+
* absent, `true`, or a typo'd non-boolean like `"false"` (a string) — must stay `true`, never
|
|
149
|
+
* silently suppress on a value nobody intended as the opt-out. */
|
|
150
|
+
function configBooleanDefaultTrue(config, key) {
|
|
151
|
+
return config[key] !== false;
|
|
152
|
+
}
|
|
153
|
+
/** Project a parsed marker onto the six fields the resolvers read. `type` is lowercased here, as
|
|
154
|
+
* the old attribute pattern's `i` flag did — a config is hand-authored JSON, so `"Widget"` must keep
|
|
155
|
+
* reaching the same resolver `"widget"` does. */
|
|
156
|
+
function toEmbedRef(marker) {
|
|
157
|
+
return {
|
|
158
|
+
type: marker.type.toLowerCase(),
|
|
159
|
+
id: normalizeEmbedId(configString(marker.config, "id")),
|
|
160
|
+
slug: normalizeEmbedSlug(configString(marker.config, "slug")),
|
|
161
|
+
name: configString(marker.config, "name"),
|
|
162
|
+
variant: configString(marker.config, "variant"),
|
|
163
|
+
header: configBooleanDefaultTrue(marker.config, "header"),
|
|
164
|
+
...(marker.config.mode === "html" ? { mode: "html" } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Every embed reference in `html`, in document order, truncated at
|
|
169
|
+
* {@link MAX_HTML_EMBEDS_PER_PAGE}. Pure — no I/O, never throws. Shared by the render-time resolver
|
|
170
|
+
* (`resolver-service.ts`'s `resolveHtmlPageEmbeds`, which needs to know what to batch-load) and
|
|
171
|
+
* anything that needs to enumerate a page's embed set without rendering it.
|
|
172
|
+
*
|
|
173
|
+
* A marker whose config does not parse is simply absent from the result — render-time stays
|
|
174
|
+
* forgiving (warn + degrade, never throw), and the unparseable marker's own markup survives
|
|
175
|
+
* untouched into the output because {@link substituteHtmlEmbeds} cannot see it either. The LOUD
|
|
176
|
+
* treatment of `rejected` belongs to `core/entry-refs/extractor.ts` (the integrity path a dropped
|
|
177
|
+
* reference would let a delete proceed against) and to the write chokepoint, not here.
|
|
178
|
+
*
|
|
179
|
+
* @complexity O(n) over `html`'s length for the shared scan, plus O(min(k, cap)) to build the
|
|
180
|
+
* returned array, where k is the number of markers.
|
|
181
|
+
* @overallScore 100
|
|
182
|
+
*/
|
|
183
|
+
export function scanHtmlEmbeds({ html }, _optional = {}) {
|
|
184
|
+
const refs = [];
|
|
185
|
+
for (const marker of scanEmbedMarkers({ html: normalizeLegacyWidgetMarkers({ html }) }).markers) {
|
|
186
|
+
refs.push(toEmbedRef(marker));
|
|
187
|
+
if (refs.length >= MAX_HTML_EMBEDS_PER_PAGE)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
return refs;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Tag names the `"media"` resolver's own rendered output can itself be (`render.ts`'s
|
|
194
|
+
* `renderImageTag`/`renderVideoTag` emit a bare `<img>`/`<video>` tag; audio is listed for the same
|
|
195
|
+
* reason ahead of an eventual player). If a `"media"` marker's OWN authored tag is one of these,
|
|
196
|
+
* splicing the resolved tag INSIDE it would nest a rendered `<video>`/`<img>` inside another
|
|
197
|
+
* same-named element — not meaningful, playable markup. Falling back to whole-element replace here is
|
|
198
|
+
* the pre-existing behavior, unchanged for this case.
|
|
199
|
+
*
|
|
200
|
+
* Nothing in this codebase authors a `"media"` marker directly on one of these tags today — every
|
|
201
|
+
* real example (the theme-authoring guide, the shipped `basic` theme, this bug's own report) wraps
|
|
202
|
+
* the marker in a container (`div`, `nav`, …) — but {@link MARKER_PATTERN} in `core/embeds/marker.ts`
|
|
203
|
+
* matches any `[a-z]+` tag name, so the grammar does not forbid it. This check is defensive, not a
|
|
204
|
+
* response to an observed case.
|
|
205
|
+
*
|
|
206
|
+
* **`"media"`-only.** `"post"`/`"content"` need no equivalent set: both resolve exclusively through
|
|
207
|
+
* `render.ts`'s `renderWidgetPostContent` (or the `widget-placeholder` div on a miss), and every
|
|
208
|
+
* shape either can produce is `<div>`-rooted — nesting a `<div>` inside a same-named `<div>` marker
|
|
209
|
+
* is ordinary, meaningful markup (unlike a `<video>` inside a `<video>`), so there is no collision to
|
|
210
|
+
* guard against for those two types.
|
|
211
|
+
*/
|
|
212
|
+
const SELF_RENDERING_MEDIA_TAGS = new Set(["img", "video", "audio"]);
|
|
213
|
+
/**
|
|
214
|
+
* Attribute names D2 (2026-09-16 embed-attributes plan) says mean something on a rendered
|
|
215
|
+
* `<video>`/`<img>` and NOTHING on a `<div>`/other wrapper tag — so when a `"media"` marker is
|
|
216
|
+
* authored on a non-self-rendering wrapper, an attribute in this set moves off the wrapper and onto
|
|
217
|
+
* the resolved media element; every other authored attribute (`class`, `style`, `id`, `data-*`,
|
|
218
|
+
* `aria-*`, an unknown name) stays on the wrapper. See {@link substituteHtmlEmbeds}'s own doc for how
|
|
219
|
+
* this partition is used, and `render.ts`'s merge helper for how `EmbedOccurrence.elementAttributes`
|
|
220
|
+
* actually lands on the rendered tag (D3's precedence rules).
|
|
221
|
+
*
|
|
222
|
+
* `controls` is listed here (it means nothing on a `<div>` either) even though it is ALSO read
|
|
223
|
+
* separately into {@link EmbedOccurrence.controls} — see {@link resolveMarkerControls}'s own doc for
|
|
224
|
+
* why a raw `controls` attribute is never itself re-emitted onto the rendered element.
|
|
225
|
+
*/
|
|
226
|
+
export const MEDIA_ELEMENT_ATTRIBUTE_NAMES = new Set([
|
|
227
|
+
"autoplay",
|
|
228
|
+
"muted",
|
|
229
|
+
"loop",
|
|
230
|
+
"playsinline",
|
|
231
|
+
"controls",
|
|
232
|
+
"controlslist",
|
|
233
|
+
"disablepictureinpicture",
|
|
234
|
+
"disableremoteplayback",
|
|
235
|
+
"poster",
|
|
236
|
+
"preload",
|
|
237
|
+
"crossorigin",
|
|
238
|
+
"alt",
|
|
239
|
+
"loading",
|
|
240
|
+
"decoding",
|
|
241
|
+
"fetchpriority",
|
|
242
|
+
"sizes",
|
|
243
|
+
"referrerpolicy",
|
|
244
|
+
"width",
|
|
245
|
+
"height",
|
|
246
|
+
]);
|
|
247
|
+
/**
|
|
248
|
+
* Embed types whose resolved replacement is spliced INSIDE the marker's own tag/attrs
|
|
249
|
+
* ({@link withInnerContentFinal}) rather than replacing the whole element — the generalization
|
|
250
|
+
* (2026-08-24, same day as the original `"media"`-only fix) of the reasoning that fix established,
|
|
251
|
+
* applied per type rather than blanket. `"media"` itself is no longer driven through this constant
|
|
252
|
+
* (2026-09-16): it needs its own attribute-partitioning logic (see {@link substituteHtmlEmbeds}), not
|
|
253
|
+
* a plain splice-or-not decision, so it is handled in its own branch instead.
|
|
254
|
+
*
|
|
255
|
+
* - **`content`** (generalized 2026-08-24): `development/docs/architecture/embed-type-inventory.md`
|
|
256
|
+
* names the realistic authored shape directly — `<main class="page-body"
|
|
257
|
+
* data-embed-config='{"type":"content"}'></main>` — and already documents that the SAME marker's
|
|
258
|
+
* `"html"`-format half (`pages.ts`'s `resolveHtmlFormatContentMarkers`, a separate pre-splice pass
|
|
259
|
+
* outside this function) uses `withInnerContentFinal` for exactly this reason. Before this change
|
|
260
|
+
* the `"doc"`-format half reaching THIS function (via the registry resolver,
|
|
261
|
+
* `resolveContentTypeEmbeds`) disagreed with its own sibling path and discarded the wrapper —
|
|
262
|
+
* generalizing here removes that inconsistency rather than introducing a new behavior.
|
|
263
|
+
* - **`post`** (generalized 2026-08-24): resolves through the identical `"post-content"` IR/render
|
|
264
|
+
* function `content` does (`renderWidgetPostContent` — two sibling `<div>`s, header and body, or
|
|
265
|
+
* the placeholder `<div>` on a miss). Since the resolved shape is byte-for-byte the same producer,
|
|
266
|
+
* the same "an author plausibly wraps this in their own spotlight/teaser container" reasoning
|
|
267
|
+
* applies without a separate justification.
|
|
268
|
+
* - **`widget` stays excluded from THIS constant** (it has its own 2026-09-16 rule instead — see this
|
|
269
|
+
* file's header and {@link withElementKeptIfAttributed}). Unlike `post`/`content`, a widget's
|
|
270
|
+
* resolved root tag is NOT a small, closed set this function can reason about: `WIDGET_IR_RENDERERS`
|
|
271
|
+
* (`server/http/site/render.ts`) alone spans `<div>`, `<ul>`, `<li>`, `<nav>`, and `<form>` across
|
|
272
|
+
* its registered component ids, and `resolver-service.ts`'s own doc states adding a new widget type
|
|
273
|
+
* is meant to be cheap — a future one can introduce any root tag. Several of today's shapes are also
|
|
274
|
+
* written to BE the final element at the marker's position rather than content for a further
|
|
275
|
+
* wrapper: `entry-summary`'s bare `<li>` is meant as a direct child of a `<ul>`, and
|
|
276
|
+
* `contact-form`'s `<form>` nested inside an author's own `<form>` marker tag would be invalid HTML
|
|
277
|
+
* — the same kind of self-nesting hazard {@link SELF_RENDERING_MEDIA_TAGS} guards for media, but
|
|
278
|
+
* spanning a tag set this function cannot enumerate (it only ever sees the resolved STRING
|
|
279
|
+
* `resolve()` returns, never the `componentId` that produced it). Unconditionally wrapping every
|
|
280
|
+
* widget the way `post`/`content` are wrapped would risk exactly that invalid nesting; keeping a
|
|
281
|
+
* BARE widget marker on the pre-existing whole-element replace avoids guessing at the mapping, while
|
|
282
|
+
* an author who explicitly attributed the marker gets their element kept (their choice, their risk).
|
|
283
|
+
*/
|
|
284
|
+
const WRAPPER_PRESERVING_EMBED_TYPES = new Set(["post", "content"]);
|
|
285
|
+
/**
|
|
286
|
+
* A wrapper-preserving-type (`"post"`/`"content"`) marker's own resolved replacement: the resolved
|
|
287
|
+
* markup spliced inside the marker's own tag/attrs via {@link withInnerContentFinal} so an authored
|
|
288
|
+
* wrapper (style, class, id, aria-*) survives in the rendered output — `withInnerContentFinal`, not
|
|
289
|
+
* the plainer {@link withInnerContent}, because this output is the final HTML a visitor receives:
|
|
290
|
+
* leaving `data-embed-config` on the rebuilt tag would ship a dead JSON attribute into that markup (an
|
|
291
|
+
* existing invariant `render.test.ts` already pins — the rendered page must never carry
|
|
292
|
+
* `data-embed-config`). `withInnerContent` stays correct for `static-render.ts`'s menu markers, which
|
|
293
|
+
* are theme-authored scaffolding, not this function's final render output.
|
|
294
|
+
*
|
|
295
|
+
* `"media"` no longer reaches this function (2026-09-16) — its own attribute-partitioning branch in
|
|
296
|
+
* {@link substituteHtmlEmbeds} replaced the self-rendering-tag check this function used to make for it.
|
|
297
|
+
*/
|
|
298
|
+
function spliceWrapperPreservingReplacement(marker, resolved) {
|
|
299
|
+
return withInnerContentFinal({ marker: marker, inner: resolved });
|
|
300
|
+
}
|
|
301
|
+
const NO_ELEMENT_ATTRIBUTES = [];
|
|
302
|
+
/** The occurrence handed to `resolve` for every type this function does not partition attributes
|
|
303
|
+
* for — `"post"`, `"content"`, and the widget/fallback branch. Shared so those branches allocate
|
|
304
|
+
* nothing per marker. */
|
|
305
|
+
const DEFAULT_OCCURRENCE = { elementAttributes: NO_ELEMENT_ATTRIBUTES, controls: true };
|
|
306
|
+
/**
|
|
307
|
+
* Whether a `"media"` marker's `controls` attribute turns controls OFF — the 2026-09-16 owner
|
|
308
|
+
* decision: controls-off is an HTML ATTRIBUTE authored on the marker (`controls="false"`), not a JSON
|
|
309
|
+
* config key. `false` only when the attribute's value, trimmed and lowercased, is the literal string
|
|
310
|
+
* `"false"`; a bare `controls` (no value), any other value (e.g. a typo), or no `controls` attribute
|
|
311
|
+
* at all all keep controls ON — the pre-existing default. Deliberately never itself re-emitted onto
|
|
312
|
+
* the rendered tag: `controls` is an HTML boolean attribute a browser honors by PRESENCE alone, so a
|
|
313
|
+
* literal `controls="false"` in the output would still show controls, exactly the bug this rule
|
|
314
|
+
* exists to prevent. This is the ONLY place that inspects a `"controls"`-named {@link MarkerAttribute}
|
|
315
|
+
* — {@link planMediaMarkerAttributes} strips any `"controls"` entry out of
|
|
316
|
+
* {@link EmbedOccurrence.elementAttributes} unconditionally, so `render.ts`'s merge helper never even
|
|
317
|
+
* sees one; it renders controls-on-or-off purely from the `controls` boolean this function returns.
|
|
318
|
+
*
|
|
319
|
+
* @complexity O(n) in the attribute count (one linear find).
|
|
320
|
+
*/
|
|
321
|
+
function resolveMarkerControls(attrs) {
|
|
322
|
+
const controlsAttr = attrs.find((attr) => attr.name === "controls");
|
|
323
|
+
if (!controlsAttr || controlsAttr.value === null)
|
|
324
|
+
return true;
|
|
325
|
+
return controlsAttr.value.trim().toLowerCase() !== "false";
|
|
326
|
+
}
|
|
327
|
+
function planMediaMarkerAttributes(marker) {
|
|
328
|
+
const parsed = parseMarkerAttributes({ marker: marker });
|
|
329
|
+
const controls = resolveMarkerControls(parsed);
|
|
330
|
+
// `controls` is NEVER included in `elementAttributes` (2026-09-16, tightened per owner review) —
|
|
331
|
+
// its effect is carried exclusively by `controls` above. A browser honors an HTML boolean attribute
|
|
332
|
+
// by PRESENCE alone regardless of value, so leaving a `controls`/`controls="false"` entry inside
|
|
333
|
+
// `elementAttributes` for a renderer to forward verbatim would risk shipping a literal
|
|
334
|
+
// `controls="false"` that still shows controls — stripping it here, once, removes that hazard
|
|
335
|
+
// entirely rather than trusting every downstream renderer to skip the name correctly.
|
|
336
|
+
const withoutControls = (attrs) => attrs.filter((attr) => attr.name !== "controls");
|
|
337
|
+
if (SELF_RENDERING_MEDIA_TAGS.has(marker.tag.toLowerCase())) {
|
|
338
|
+
// Collapse case: there is no wrapper, so every authored attribute (besides controls) hands to the
|
|
339
|
+
// occurrence.
|
|
340
|
+
return { occurrence: { elementAttributes: withoutControls(parsed), controls }, wrapperAttributes: NO_ELEMENT_ATTRIBUTES, movedAnyAttribute: true };
|
|
341
|
+
}
|
|
342
|
+
const mediaElementAttrs = parsed.filter((attr) => MEDIA_ELEMENT_ATTRIBUTE_NAMES.has(attr.name));
|
|
343
|
+
const wrapperAttributes = parsed.filter((attr) => !MEDIA_ELEMENT_ATTRIBUTE_NAMES.has(attr.name));
|
|
344
|
+
// `movedAnyAttribute` is computed from the PRE-strip count: a controls-only marker still moved an
|
|
345
|
+
// attribute off the wrapper (controls means nothing on a div either), even though `elementAttributes`
|
|
346
|
+
// itself ends up empty after `controls` is stripped out.
|
|
347
|
+
return { occurrence: { elementAttributes: withoutControls(mediaElementAttrs), controls }, wrapperAttributes, movedAnyAttribute: mediaElementAttrs.length > 0 };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Replaces every embed placeholder in `html` with `resolve(ref, occurrence)`'s return value.
|
|
351
|
+
* `resolve` is synchronous and pure from this function's own perspective — the caller is expected
|
|
352
|
+
* to have already batch-resolved every ref it cares about (mirroring how `render.ts`'s
|
|
353
|
+
* `renderDocNode` receives an already-resolved `inlineResolved` map rather than doing its own I/O);
|
|
354
|
+
* a ref `resolve` doesn't recognize (unknown type, never loaded, or beyond
|
|
355
|
+
* {@link MAX_HTML_EMBEDS_PER_PAGE}) is `resolve`'s own call to degrade safely, not something this
|
|
356
|
+
* function special-cases — every caller here (`render.ts`'s `renderHtmlPageBody`) already returns
|
|
357
|
+
* the REQ-28 placeholder for an unresolved ref, which is exactly what "no cap-aware branch needed
|
|
358
|
+
* here" relies on.
|
|
359
|
+
*
|
|
360
|
+
* `resolve` returning `undefined` means **leave this marker exactly as authored**, forwarded straight
|
|
361
|
+
* to `substituteMarkers`' own invariant of the same name. That is not a convenience: since the
|
|
362
|
+
* 2026-08-10 unification this stage SEES markers it does not own (a theme's `partial`/`menu`), and a
|
|
363
|
+
* later stage resolves them. Substituting anything at all over one of those — including the REQ-28
|
|
364
|
+
* placeholder — silently deletes a nav. Which types this stage owns is `resolver-service.ts`'s
|
|
365
|
+
* `isPageEmbedType` to answer; this function only carries the answer through.
|
|
366
|
+
*
|
|
367
|
+
* Per-type handling (2026-09-16):
|
|
368
|
+
* - **`"media"`**: {@link planMediaMarkerAttributes} computes the occurrence AND the wrapper split.
|
|
369
|
+
* On the marker's own self-rendering tag ({@link SELF_RENDERING_MEDIA_TAGS}) the replacement is
|
|
370
|
+
* returned as-is (no wrapper, unchanged from before). On any other tag: if nothing moved, the
|
|
371
|
+
* wrapper is rebuilt via the byte-identical {@link withInnerContentFinal} path; otherwise via
|
|
372
|
+
* {@link withInnerContentAndAttributes} with only the reduced wrapper attribute list.
|
|
373
|
+
* - **`"post"`/`"content"`** ({@link WRAPPER_PRESERVING_EMBED_TYPES}): spliced via
|
|
374
|
+
* {@link spliceWrapperPreservingReplacement}, occurrence attributes always empty.
|
|
375
|
+
* - **Every other type** (`"widget"`, and any type this stage does not itself own but still
|
|
376
|
+
* resolves): {@link withElementKeptIfAttributed} — a bare marker keeps the pre-existing
|
|
377
|
+
* whole-element replace; an attributed one keeps its element around the resolved output.
|
|
378
|
+
*
|
|
379
|
+
* @complexity O(n) over `html`'s length for the shared scan, plus O(k) per `"media"` marker in its own
|
|
380
|
+
* attribute count (one two-way partition), O(1) per marker otherwise.
|
|
381
|
+
* @overallScore 100
|
|
382
|
+
*/
|
|
383
|
+
export function substituteHtmlEmbeds({ html, resolve }, _optional = {}) {
|
|
384
|
+
return substituteMarkers({ html: normalizeLegacyWidgetMarkers({ html }), resolve: (marker) => {
|
|
385
|
+
const ref = toEmbedRef(marker);
|
|
386
|
+
if (ref.type === "media") {
|
|
387
|
+
const plan = planMediaMarkerAttributes(marker);
|
|
388
|
+
const replacement = resolve(ref, plan.occurrence);
|
|
389
|
+
if (replacement === undefined)
|
|
390
|
+
return undefined;
|
|
391
|
+
if (SELF_RENDERING_MEDIA_TAGS.has(marker.tag.toLowerCase()))
|
|
392
|
+
return replacement;
|
|
393
|
+
return plan.movedAnyAttribute
|
|
394
|
+
? withInnerContentAndAttributes({ marker: marker, attrs: plan.wrapperAttributes, inner: replacement })
|
|
395
|
+
: withInnerContentFinal({ marker: marker, inner: replacement });
|
|
396
|
+
}
|
|
397
|
+
const replacement = resolve(ref, DEFAULT_OCCURRENCE);
|
|
398
|
+
if (replacement === undefined)
|
|
399
|
+
return undefined;
|
|
400
|
+
// A missing widget contributes nothing, including its authored wrapper/fallback content.
|
|
401
|
+
if (ref.type === "widget" && replacement === "")
|
|
402
|
+
return "";
|
|
403
|
+
if (WRAPPER_PRESERVING_EMBED_TYPES.has(ref.type))
|
|
404
|
+
return spliceWrapperPreservingReplacement(marker, replacement);
|
|
405
|
+
return withElementKeptIfAttributed({ marker: marker, inner: replacement });
|
|
406
|
+
} });
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=html-embeds.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Adapt the old CMS widget attributes to the shared JSON marker vocabulary at render time.
|
|
2
|
+
* Source offsets preserve authored HTML; serialization would rewrite unrelated markup. parse5
|
|
3
|
+
* keeps comments, script/style text and quoted attribute values out of the element scan.
|
|
4
|
+
* Only widgets need this compatibility rule; other embed types keep the current contract. */
|
|
5
|
+
export declare function normalizeLegacyWidgetMarkers({ html }: {
|
|
6
|
+
html: string;
|
|
7
|
+
}, _optional?: Record<string, never>): string;
|
|
8
|
+
//# sourceMappingURL=legacy-widget-markers.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseFragment } from "parse5";
|
|
2
|
+
/** Adapt the old CMS widget attributes to the shared JSON marker vocabulary at render time.
|
|
3
|
+
* Source offsets preserve authored HTML; serialization would rewrite unrelated markup. parse5
|
|
4
|
+
* keeps comments, script/style text and quoted attribute values out of the element scan.
|
|
5
|
+
* Only widgets need this compatibility rule; other embed types keep the current contract. */
|
|
6
|
+
export function normalizeLegacyWidgetMarkers({ html }, _optional = {}) {
|
|
7
|
+
if (!/data-embed-type/i.test(html))
|
|
8
|
+
return html;
|
|
9
|
+
const stack = [...parseFragment(html, { sourceCodeLocationInfo: true }).childNodes];
|
|
10
|
+
const edits = [];
|
|
11
|
+
while (stack.length) {
|
|
12
|
+
const node = stack.pop();
|
|
13
|
+
if (!("tagName" in node))
|
|
14
|
+
continue;
|
|
15
|
+
stack.push(...node.childNodes);
|
|
16
|
+
const type = node.attrs.find((attr) => attr.name === "data-embed-type");
|
|
17
|
+
if (type?.value.toLowerCase() !== "widget")
|
|
18
|
+
continue;
|
|
19
|
+
const locations = node.sourceCodeLocation?.attrs;
|
|
20
|
+
const typeLocation = locations?.["data-embed-type"];
|
|
21
|
+
if (!typeLocation)
|
|
22
|
+
continue;
|
|
23
|
+
const id = node.attrs.find((attr) => attr.name === "data-embed-id")?.value;
|
|
24
|
+
const hasConfig = node.attrs.some((attr) => attr.name === "data-embed-config");
|
|
25
|
+
// JSON's own escapes keep an id containing a quote/ampersand literal inside a single-quoted
|
|
26
|
+
// attribute. The shared scanner intentionally reads that quoting verbatim, without HTML decoding.
|
|
27
|
+
const config = JSON.stringify({ type: "widget", ...(id !== undefined ? { id } : {}) })
|
|
28
|
+
.replace(/'/g, "\\u0027").replace(/&/g, "\\u0026");
|
|
29
|
+
edits.push({ start: typeLocation.startOffset, end: typeLocation.endOffset,
|
|
30
|
+
replacement: hasConfig ? "" : `data-embed-config='${config}'` });
|
|
31
|
+
const idLocation = locations?.["data-embed-id"];
|
|
32
|
+
if (idLocation) {
|
|
33
|
+
let start = idLocation.startOffset;
|
|
34
|
+
while (start > 0 && /\s/.test(html[start - 1]))
|
|
35
|
+
start -= 1;
|
|
36
|
+
edits.push({ start, end: idLocation.endOffset, replacement: "" });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let output = html;
|
|
40
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
41
|
+
output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=legacy-widget-markers.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
2
|
+
import { type ResolveHtmlPageEmbedsDeps } from "../resolver-service.js";
|
|
3
|
+
import type { WidgetRenderIR } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolved embed IR, keyed by embed type then by the referenced id (`resolved.get(ref.type)?.get(
|
|
6
|
+
* ref.id)`). A type absent from the outer map means either nothing of that type was found on the
|
|
7
|
+
* page or the type has no registered resolver (`render.ts`'s `renderHtmlPageBody` treats both
|
|
8
|
+
* identically — a REQ-28 placeholder, per this file's `HTML_EMBED_RESOLVERS` doc).
|
|
9
|
+
*/
|
|
10
|
+
export type ResolveHtmlPageEmbedsResult = ReadonlyMap<string, ReadonlyMap<string, WidgetRenderIR>>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves every `data-embed-type` placeholder (`html-embeds.ts`'s `scanHtmlEmbeds`) found in an
|
|
13
|
+
* `"html"`-format Page's `body_html`, dispatching each embed type to its registered resolver in
|
|
14
|
+
* {@link HTML_EMBED_RESOLVERS}.
|
|
15
|
+
*
|
|
16
|
+
* Never throws (REQ-27's contract, carried over unchanged): a malformed, missing, disabled, wrong-
|
|
17
|
+
* type, unknown-type, or duplicate-beyond-{@link MAX_HTML_EMBEDS_PER_PAGE} reference degrades to the
|
|
18
|
+
* REQ-28 placeholder exactly the way every other widget-resolution failure already does — the caller
|
|
19
|
+
* (`render.ts`'s `renderHtmlPageBody`) never special-cases "this ref failed to resolve" versus "this
|
|
20
|
+
* ref does not exist" versus "this ref was never attempted" — **for a type this stage owns**
|
|
21
|
+
* ({@link HTML_EMBED_RESOLVERS}). A type it deliberately does not own ({@link THEME_OWNED_MARKER_TYPES})
|
|
22
|
+
* is absent from the returned map for a different reason and reaches a different outcome downstream:
|
|
23
|
+
* `renderHtmlPageBody`'s `isPageEmbedType` check leaves that marker untouched for `static-render.ts`'s
|
|
24
|
+
* later pass to resolve, not a placeholder. Do not read "absent from this map" as "renders as a
|
|
25
|
+
* placeholder" — that conflation is exactly what made this stage's own logging misleading (see
|
|
26
|
+
* `THEME_OWNED_MARKER_TYPES`'s doc for the live incident this caused).
|
|
27
|
+
*
|
|
28
|
+
* @complexity O(e) over the page's embed-reference count (capped at `MAX_HTML_EMBEDS_PER_PAGE`) for
|
|
29
|
+
* the scan plus grouping, plus each registered type present incurring its own resolver's cost (the
|
|
30
|
+
* widget path: one batched widget-listing query plus at most one `resolveWidgetType` call per
|
|
31
|
+
* distinct widget type) — the same O(1)-typed per-type cost shape the pre-restructuring function
|
|
32
|
+
* already carried, REQ-24 in spirit.
|
|
33
|
+
* @overallScore 100
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveHtmlPageEmbeds(required: {
|
|
36
|
+
deps: ResolveHtmlPageEmbedsDeps;
|
|
37
|
+
input: {
|
|
38
|
+
readonly workspaceId: UUID;
|
|
39
|
+
readonly html: string;
|
|
40
|
+
};
|
|
41
|
+
}, _optional?: Record<string, never>): Promise<ResolveHtmlPageEmbedsResult>;
|
|
42
|
+
//# sourceMappingURL=resolve-html-page-embeds.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HTML_EMBED_RESOLVERS, THEME_OWNED_MARKER_TYPES } from "../resolver-service.js";
|
|
2
|
+
import { scanHtmlEmbeds } from "./html-embeds.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves every `data-embed-type` placeholder (`html-embeds.ts`'s `scanHtmlEmbeds`) found in an
|
|
5
|
+
* `"html"`-format Page's `body_html`, dispatching each embed type to its registered resolver in
|
|
6
|
+
* {@link HTML_EMBED_RESOLVERS}.
|
|
7
|
+
*
|
|
8
|
+
* Never throws (REQ-27's contract, carried over unchanged): a malformed, missing, disabled, wrong-
|
|
9
|
+
* type, unknown-type, or duplicate-beyond-{@link MAX_HTML_EMBEDS_PER_PAGE} reference degrades to the
|
|
10
|
+
* REQ-28 placeholder exactly the way every other widget-resolution failure already does — the caller
|
|
11
|
+
* (`render.ts`'s `renderHtmlPageBody`) never special-cases "this ref failed to resolve" versus "this
|
|
12
|
+
* ref does not exist" versus "this ref was never attempted" — **for a type this stage owns**
|
|
13
|
+
* ({@link HTML_EMBED_RESOLVERS}). A type it deliberately does not own ({@link THEME_OWNED_MARKER_TYPES})
|
|
14
|
+
* is absent from the returned map for a different reason and reaches a different outcome downstream:
|
|
15
|
+
* `renderHtmlPageBody`'s `isPageEmbedType` check leaves that marker untouched for `static-render.ts`'s
|
|
16
|
+
* later pass to resolve, not a placeholder. Do not read "absent from this map" as "renders as a
|
|
17
|
+
* placeholder" — that conflation is exactly what made this stage's own logging misleading (see
|
|
18
|
+
* `THEME_OWNED_MARKER_TYPES`'s doc for the live incident this caused).
|
|
19
|
+
*
|
|
20
|
+
* @complexity O(e) over the page's embed-reference count (capped at `MAX_HTML_EMBEDS_PER_PAGE`) for
|
|
21
|
+
* the scan plus grouping, plus each registered type present incurring its own resolver's cost (the
|
|
22
|
+
* widget path: one batched widget-listing query plus at most one `resolveWidgetType` call per
|
|
23
|
+
* distinct widget type) — the same O(1)-typed per-type cost shape the pre-restructuring function
|
|
24
|
+
* already carried, REQ-24 in spirit.
|
|
25
|
+
* @overallScore 100
|
|
26
|
+
*/
|
|
27
|
+
export async function resolveHtmlPageEmbeds(required, _optional = {}) {
|
|
28
|
+
const { deps, input } = required;
|
|
29
|
+
const context = { workspaceId: input.workspaceId, preview: false };
|
|
30
|
+
const refs = scanHtmlEmbeds({ html: input.html });
|
|
31
|
+
const refsByType = new Map();
|
|
32
|
+
for (const ref of refs) {
|
|
33
|
+
const list = refsByType.get(ref.type) ?? [];
|
|
34
|
+
list.push(ref);
|
|
35
|
+
refsByType.set(ref.type, list);
|
|
36
|
+
}
|
|
37
|
+
const result = new Map();
|
|
38
|
+
for (const [type, typeRefs] of refsByType) {
|
|
39
|
+
const resolver = HTML_EMBED_RESOLVERS[type];
|
|
40
|
+
if (!resolver) {
|
|
41
|
+
// A theme-structural type (`menu`/`partial`) is not a failure at THIS stage — it is deferred,
|
|
42
|
+
// by design, to `static-render.ts`'s later pass (see `THEME_OWNED_MARKER_TYPES`'s own doc). Only
|
|
43
|
+
// a type genuinely unowned anywhere is worth the loud warning.
|
|
44
|
+
if (!THEME_OWNED_MARKER_TYPES.has(type)) {
|
|
45
|
+
console.warn("[widgets] resolveHtmlPageEmbeds: unknown embed type, every occurrence degrades to the placeholder", {
|
|
46
|
+
type,
|
|
47
|
+
workspaceId: input.workspaceId,
|
|
48
|
+
occurrences: typeRefs.length,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
result.set(type, await resolver(typeRefs, deps, context));
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=resolve-html-page-embeds.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./registry.js";
|
|
3
|
+
export * from "./config-validation.js";
|
|
4
|
+
export * from "./embed-validation.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export * from "./entry-payload.js";
|
|
7
|
+
export * from "./concurrency.js";
|
|
8
|
+
export * from "./ports.js";
|
|
9
|
+
export * from "./host-ports.js";
|
|
10
|
+
export * from "./page-embed-types.js";
|
|
11
|
+
export * from "./authorize-helper.js";
|
|
12
|
+
export * from "./where-used.js";
|
|
13
|
+
export * from "./read-service.js";
|
|
14
|
+
export * from "./write-service.js";
|
|
15
|
+
export * from "./embed-service.js";
|
|
16
|
+
export * from "./region-area-service.js";
|
|
17
|
+
export * from "./repo.memory.js";
|
|
18
|
+
export { resolvePageWidgets } from "./resolver-service.js";
|
|
19
|
+
export type { ResolvePageWidgetsDeps, ResolvePageWidgetsInput, ResolvePageWidgetsRequired, ResolvePageWidgetsResult } from "./resolver-service.js";
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./registry.js";
|
|
3
|
+
export * from "./config-validation.js";
|
|
4
|
+
export * from "./embed-validation.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export * from "./entry-payload.js";
|
|
7
|
+
export * from "./concurrency.js";
|
|
8
|
+
export * from "./ports.js";
|
|
9
|
+
export * from "./host-ports.js";
|
|
10
|
+
export * from "./page-embed-types.js";
|
|
11
|
+
export * from "./authorize-helper.js";
|
|
12
|
+
export * from "./where-used.js";
|
|
13
|
+
export * from "./read-service.js";
|
|
14
|
+
export * from "./write-service.js";
|
|
15
|
+
export * from "./embed-service.js";
|
|
16
|
+
export * from "./region-area-service.js";
|
|
17
|
+
export * from "./repo.memory.js";
|
|
18
|
+
export { resolvePageWidgets } from "./resolver-service.js";
|
|
19
|
+
//# sourceMappingURL=index.js.map
|