@jini-ai/cms 0.5.2 → 0.5.4

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.
Files changed (261) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/forms/delete-submission.d.ts +38 -0
  46. package/dist/forms/delete-submission.js +25 -0
  47. package/dist/forms/duplicate-slug.d.ts +81 -0
  48. package/dist/forms/duplicate-slug.js +115 -0
  49. package/dist/forms/errors.d.ts +53 -0
  50. package/dist/forms/errors.js +25 -0
  51. package/dist/forms/express/handler.d.ts +59 -0
  52. package/dist/forms/express/handler.js +217 -0
  53. package/dist/forms/express/index.d.ts +2 -0
  54. package/dist/forms/express/index.js +2 -0
  55. package/dist/forms/fields-json.d.ts +14 -0
  56. package/dist/forms/fields-json.js +11 -0
  57. package/dist/forms/forms.d.ts +59 -0
  58. package/dist/forms/forms.js +213 -0
  59. package/dist/forms/html/authoring-adapter.d.ts +34 -0
  60. package/dist/forms/html/authoring-adapter.js +48 -0
  61. package/dist/forms/html/html-authoring.d.ts +12 -0
  62. package/dist/forms/html/html-authoring.js +133 -0
  63. package/dist/forms/html/html-submission-adapter.d.ts +10 -0
  64. package/dist/forms/html/html-submission-adapter.js +15 -0
  65. package/dist/forms/html/index.d.ts +4 -0
  66. package/dist/forms/html/index.js +4 -0
  67. package/dist/forms/html-render.d.ts +33 -0
  68. package/dist/forms/html-render.js +31 -0
  69. package/dist/forms/index.d.ts +17 -0
  70. package/dist/forms/index.js +17 -0
  71. package/dist/forms/notify-subscriber.d.ts +32 -0
  72. package/dist/forms/notify-subscriber.js +60 -0
  73. package/dist/forms/ports.d.ts +168 -0
  74. package/dist/forms/ports.js +2 -0
  75. package/dist/forms/rate-limit-key.d.ts +8 -0
  76. package/dist/forms/rate-limit-key.js +7 -0
  77. package/dist/forms/repo.memory.d.ts +120 -0
  78. package/dist/forms/repo.memory.js +188 -0
  79. package/dist/forms/sql/index.d.ts +5 -0
  80. package/dist/forms/sql/index.js +5 -0
  81. package/dist/forms/sql/repo.d.ts +94 -0
  82. package/dist/forms/sql/repo.js +175 -0
  83. package/dist/forms/sql/repo.rows.d.ts +33 -0
  84. package/dist/forms/sql/repo.rows.js +64 -0
  85. package/dist/forms/sql/submission-ip-retention-repo.d.ts +9 -0
  86. package/dist/forms/sql/submission-ip-retention-repo.js +40 -0
  87. package/dist/forms/sql/tables.d.ts +32 -0
  88. package/dist/forms/sql/tables.js +2 -0
  89. package/dist/forms/submission-attempts.d.ts +70 -0
  90. package/dist/forms/submission-attempts.js +70 -0
  91. package/dist/forms/submission-email-validation.d.ts +8 -0
  92. package/dist/forms/submission-email-validation.js +34 -0
  93. package/dist/forms/submission-ip-retention.d.ts +30 -0
  94. package/dist/forms/submission-ip-retention.js +27 -0
  95. package/dist/forms/submit-service.d.ts +78 -0
  96. package/dist/forms/submit-service.js +109 -0
  97. package/dist/forms/types.d.ts +49 -0
  98. package/dist/forms/types.js +2 -0
  99. package/dist/forms/write-service.d.ts +83 -0
  100. package/dist/forms/write-service.js +271 -0
  101. package/dist/media/index.d.ts +0 -3
  102. package/dist/media/index.js +1 -4
  103. package/dist/media/node.d.ts +5 -0
  104. package/dist/media/node.js +6 -0
  105. package/dist/redirects/capture.d.ts +80 -0
  106. package/dist/redirects/capture.js +66 -0
  107. package/dist/redirects/express/index.d.ts +27 -0
  108. package/dist/redirects/express/index.js +40 -0
  109. package/dist/redirects/hit-sink.d.ts +56 -0
  110. package/dist/redirects/hit-sink.js +44 -0
  111. package/dist/redirects/host-config/index.d.ts +40 -0
  112. package/dist/redirects/host-config/index.js +145 -0
  113. package/dist/redirects/index.d.ts +14 -0
  114. package/dist/redirects/index.js +14 -0
  115. package/dist/redirects/matcher.d.ts +58 -0
  116. package/dist/redirects/matcher.js +144 -0
  117. package/dist/redirects/order.d.ts +30 -0
  118. package/dist/redirects/order.js +26 -0
  119. package/dist/redirects/ports.d.ts +242 -0
  120. package/dist/redirects/ports.internal.d.ts +57 -0
  121. package/dist/redirects/ports.internal.js +14 -0
  122. package/dist/redirects/ports.js +2 -0
  123. package/dist/redirects/redirects.d.ts +226 -0
  124. package/dist/redirects/redirects.js +590 -0
  125. package/dist/redirects/referrer-alias.d.ts +31 -0
  126. package/dist/redirects/referrer-alias.js +33 -0
  127. package/dist/redirects/repo.memory.d.ts +51 -0
  128. package/dist/redirects/repo.memory.js +98 -0
  129. package/dist/redirects/reserved-destination.d.ts +58 -0
  130. package/dist/redirects/reserved-destination.js +59 -0
  131. package/dist/redirects/resolver.d.ts +100 -0
  132. package/dist/redirects/resolver.js +87 -0
  133. package/dist/redirects/spa/index.d.ts +30 -0
  134. package/dist/redirects/spa/index.js +27 -0
  135. package/dist/redirects/sql/index.d.ts +4 -0
  136. package/dist/redirects/sql/index.js +4 -0
  137. package/dist/redirects/sql/repo.d.ts +82 -0
  138. package/dist/redirects/sql/repo.js +143 -0
  139. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  140. package/dist/redirects/sql/repo.rows.js +79 -0
  141. package/dist/redirects/sql/row-types.d.ts +34 -0
  142. package/dist/redirects/sql/row-types.js +2 -0
  143. package/dist/redirects/target-gate.d.ts +30 -0
  144. package/dist/redirects/target-gate.js +85 -0
  145. package/dist/redirects/types.d.ts +213 -0
  146. package/dist/redirects/types.js +42 -0
  147. package/dist/seo/absolute-url.d.ts +58 -0
  148. package/dist/seo/absolute-url.js +117 -0
  149. package/dist/seo/dom/index.d.ts +14 -0
  150. package/dist/seo/dom/index.js +52 -0
  151. package/dist/seo/errors.d.ts +35 -0
  152. package/dist/seo/errors.js +35 -0
  153. package/dist/seo/feed.d.ts +49 -0
  154. package/dist/seo/feed.js +61 -0
  155. package/dist/seo/index.d.ts +13 -0
  156. package/dist/seo/index.js +13 -0
  157. package/dist/seo/media.d.ts +57 -0
  158. package/dist/seo/media.js +134 -0
  159. package/dist/seo/page-head.d.ts +18 -0
  160. package/dist/seo/page-head.js +114 -0
  161. package/dist/seo/ports.d.ts +221 -0
  162. package/dist/seo/ports.js +27 -0
  163. package/dist/seo/seo.d.ts +73 -0
  164. package/dist/seo/seo.js +230 -0
  165. package/dist/seo/sitemap.d.ts +139 -0
  166. package/dist/seo/sitemap.js +217 -0
  167. package/dist/seo/types.d.ts +278 -0
  168. package/dist/seo/types.js +2 -0
  169. package/dist/seo/write-service.d.ts +79 -0
  170. package/dist/seo/write-service.js +273 -0
  171. package/dist/seo/xml-escape.d.ts +16 -0
  172. package/dist/seo/xml-escape.js +19 -0
  173. package/dist/settings/express/cms-adapter.d.ts +20 -0
  174. package/dist/settings/express/cms-adapter.js +124 -0
  175. package/dist/settings/express/contracts.d.ts +135 -0
  176. package/dist/settings/express/contracts.js +2 -0
  177. package/dist/settings/express/http.d.ts +10 -0
  178. package/dist/settings/express/http.js +211 -0
  179. package/dist/settings/express/index.d.ts +5 -0
  180. package/dist/settings/express/index.js +5 -0
  181. package/dist/settings/express/shared.d.ts +68 -0
  182. package/dist/settings/express/shared.js +54 -0
  183. package/dist/settings/express/stream.d.ts +24 -0
  184. package/dist/settings/express/stream.js +147 -0
  185. package/dist/settings/index.d.ts +3 -38
  186. package/dist/settings/index.js +3 -35
  187. package/dist/taxonomy/write-service.d.ts +2 -30
  188. package/dist/widgets/authorize-helper.d.ts +42 -0
  189. package/dist/widgets/authorize-helper.js +16 -0
  190. package/dist/widgets/concurrency.d.ts +12 -0
  191. package/dist/widgets/concurrency.js +44 -0
  192. package/dist/widgets/config-validation.d.ts +34 -0
  193. package/dist/widgets/config-validation.js +125 -0
  194. package/dist/widgets/embed-service.d.ts +133 -0
  195. package/dist/widgets/embed-service.js +460 -0
  196. package/dist/widgets/embed-validation.d.ts +23 -0
  197. package/dist/widgets/embed-validation.js +38 -0
  198. package/dist/widgets/entry-payload.d.ts +103 -0
  199. package/dist/widgets/entry-payload.js +160 -0
  200. package/dist/widgets/errors.d.ts +136 -0
  201. package/dist/widgets/errors.js +136 -0
  202. package/dist/widgets/host-ports.d.ts +149 -0
  203. package/dist/widgets/host-ports.js +2 -0
  204. package/dist/widgets/html/html-embeds.d.ts +247 -0
  205. package/dist/widgets/html/html-embeds.js +408 -0
  206. package/dist/widgets/html/index.d.ts +5 -0
  207. package/dist/widgets/html/index.js +4 -0
  208. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  209. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  210. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  211. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  212. package/dist/widgets/index.d.ts +20 -0
  213. package/dist/widgets/index.js +19 -0
  214. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  215. package/dist/widgets/markers/html-attributes.js +216 -0
  216. package/dist/widgets/markers/index.d.ts +3 -0
  217. package/dist/widgets/markers/index.js +3 -0
  218. package/dist/widgets/markers/marker.d.ts +496 -0
  219. package/dist/widgets/markers/marker.js +780 -0
  220. package/dist/widgets/page-embed-types.d.ts +42 -0
  221. package/dist/widgets/page-embed-types.js +42 -0
  222. package/dist/widgets/ports.d.ts +142 -0
  223. package/dist/widgets/ports.js +2 -0
  224. package/dist/widgets/read-service.d.ts +81 -0
  225. package/dist/widgets/read-service.js +79 -0
  226. package/dist/widgets/region-area-service.d.ts +110 -0
  227. package/dist/widgets/region-area-service.js +180 -0
  228. package/dist/widgets/registry.d.ts +217 -0
  229. package/dist/widgets/registry.js +172 -0
  230. package/dist/widgets/repo.memory.d.ts +41 -0
  231. package/dist/widgets/repo.memory.js +48 -0
  232. package/dist/widgets/resolver-service.d.ts +209 -0
  233. package/dist/widgets/resolver-service.js +909 -0
  234. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  235. package/dist/widgets/resolvers/contact-form.js +54 -0
  236. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  237. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  238. package/dist/widgets/resolvers/index.d.ts +65 -0
  239. package/dist/widgets/resolvers/index.js +155 -0
  240. package/dist/widgets/resolvers/menu.d.ts +42 -0
  241. package/dist/widgets/resolvers/menu.js +43 -0
  242. package/dist/widgets/resolvers/ports.d.ts +82 -0
  243. package/dist/widgets/resolvers/ports.js +2 -0
  244. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  245. package/dist/widgets/resolvers/recent-entries.js +165 -0
  246. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  247. package/dist/widgets/resolvers/taxonomy.js +34 -0
  248. package/dist/widgets/sql/index.d.ts +3 -0
  249. package/dist/widgets/sql/index.js +3 -0
  250. package/dist/widgets/sql/repo.d.ts +41 -0
  251. package/dist/widgets/sql/repo.js +82 -0
  252. package/dist/widgets/sql/tables.d.ts +13 -0
  253. package/dist/widgets/sql/tables.js +2 -0
  254. package/dist/widgets/types.d.ts +213 -0
  255. package/dist/widgets/types.js +17 -0
  256. package/dist/widgets/where-used.d.ts +41 -0
  257. package/dist/widgets/where-used.js +11 -0
  258. package/dist/widgets/write-service.d.ts +204 -0
  259. package/dist/widgets/write-service.js +332 -0
  260. package/dist/workspace/delete.d.ts +1 -1
  261. package/package.json +220 -12
@@ -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,5 @@
1
+ export * from "./html-embeds.js";
2
+ export * from "./legacy-widget-markers.js";
3
+ export * from "./resolve-html-page-embeds.js";
4
+ export type { ResolveHtmlPageEmbedsDeps } from "../resolver-service.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export * from "./html-embeds.js";
2
+ export * from "./legacy-widget-markers.js";
3
+ export * from "./resolve-html-page-embeds.js";
4
+ //# sourceMappingURL=index.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