@jini-ai/cms 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/consent/express/index.d.ts +53 -0
- package/dist/consent/express/index.js +159 -0
- package/dist/consent/express/response.d.ts +21 -0
- package/dist/consent/express/response.js +89 -0
- package/dist/consent/html/client.d.ts +6 -0
- package/dist/consent/html/client.js +135 -0
- package/dist/consent/html/gate.d.ts +29 -0
- package/dist/consent/html/gate.js +104 -0
- package/dist/consent/html/index.d.ts +4 -0
- package/dist/consent/html/index.js +3 -0
- package/dist/consent/html/render.d.ts +27 -0
- package/dist/consent/html/render.js +62 -0
- package/dist/consent/html/static.d.ts +7 -0
- package/dist/consent/html/static.js +25 -0
- package/dist/consent/html/styles.d.ts +3 -0
- package/dist/consent/html/styles.js +22 -0
- package/dist/consent/index.d.ts +3 -0
- package/dist/consent/index.js +3 -0
- package/dist/consent/node/index.d.ts +47 -0
- package/dist/consent/node/index.js +169 -0
- package/dist/consent/policy.d.ts +47 -0
- package/dist/consent/policy.js +62 -0
- package/dist/consent/types.d.ts +40 -0
- package/dist/consent/types.js +5 -0
- package/dist/content-types/agent-tools.js +1 -1
- package/dist/content-types/tool-registrations.d.ts +2 -0
- package/dist/content-types/tool-registrations.js +1 -0
- package/dist/content-types/write-service.d.ts +2 -0
- package/dist/content-types/write-service.js +3 -0
- package/dist/forms/descriptor-rules.d.ts +9 -0
- package/dist/forms/descriptor-rules.js +112 -0
- package/dist/forms/email-format.d.ts +7 -0
- package/dist/forms/email-format.js +11 -0
- package/dist/forms/express/handler.js +4 -8
- package/dist/forms/field-registry.d.ts +193 -0
- package/dist/forms/field-registry.js +231 -0
- package/dist/forms/forms.d.ts +8 -5
- package/dist/forms/forms.js +29 -46
- package/dist/forms/html/html-authoring.js +63 -13
- package/dist/forms/html/html-submission-adapter.d.ts +5 -5
- package/dist/forms/html/html-submission-adapter.js +6 -13
- package/dist/forms/html-render.d.ts +10 -3
- package/dist/forms/html-render.js +116 -14
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.js +1 -0
- package/dist/forms/repo.memory.js +4 -4
- package/dist/forms/submission-attempts.d.ts +1 -1
- package/dist/forms/submission-attempts.js +13 -2
- package/dist/forms/submission-email-validation.js +2 -4
- package/dist/forms/submit-service.d.ts +3 -0
- package/dist/forms/submit-service.js +38 -5
- package/dist/forms/types.d.ts +50 -2
- package/dist/listing/batch.d.ts +53 -0
- package/dist/listing/batch.js +57 -0
- package/dist/listing/html.d.ts +72 -0
- package/dist/listing/html.js +109 -0
- package/dist/listing/index.d.ts +5 -0
- package/dist/listing/index.js +5 -0
- package/dist/listing/paths.d.ts +32 -0
- package/dist/listing/paths.js +37 -0
- package/dist/listing/see-more-script.d.ts +16 -0
- package/dist/listing/see-more-script.js +62 -0
- package/dist/media/content-type-sniffer.d.ts +1 -1
- package/dist/media/content-type-sniffer.js +15 -0
- package/dist/media/image-transformer.sharp.js +4 -1
- package/dist/media/media-service.d.ts +4 -0
- package/dist/media/media-service.js +8 -0
- package/dist/media/node.d.ts +1 -1
- package/dist/media/node.js +1 -1
- package/dist/media/transform-registry.js +1 -1
- package/dist/media/transform-types.d.ts +2 -0
- package/dist/media/transform-types.js +3 -0
- package/dist/media/video-frames.ffmpeg.d.ts +15 -2
- package/dist/media/video-frames.ffmpeg.js +28 -13
- package/dist/navigation/agent-tools.js +20 -3
- package/dist/navigation/dropdown.d.ts +41 -0
- package/dist/navigation/dropdown.js +85 -0
- package/dist/navigation/index.d.ts +5 -1
- package/dist/navigation/index.js +4 -0
- package/dist/navigation/menu-html.d.ts +31 -0
- package/dist/navigation/menu-html.js +53 -0
- package/dist/navigation/menu-service.d.ts +9 -3
- package/dist/navigation/menu-service.js +15 -3
- package/dist/navigation/tool-registrations.d.ts +5 -0
- package/dist/navigation/tool-registrations.js +32 -2
- package/dist/navigation/types.d.ts +6 -0
- package/dist/redirects/capture.d.ts +2 -0
- package/dist/redirects/capture.js +4 -0
- package/dist/redirects/redirects.d.ts +8 -0
- package/dist/redirects/redirects.js +14 -0
- package/dist/routing/index.d.ts +5 -0
- package/dist/routing/index.js +2 -0
- package/dist/routing/ports.d.ts +51 -0
- package/dist/routing/routing.d.ts +138 -0
- package/dist/routing/routing.js +443 -0
- package/dist/routing/types.d.ts +164 -0
- package/dist/routing/types.js +2 -0
- package/dist/seo/media.d.ts +23 -0
- package/dist/seo/media.js +30 -5
- package/dist/seo/page-head.js +8 -1
- package/dist/seo/seo.d.ts +2 -0
- package/dist/seo/seo.js +11 -6
- package/dist/seo/types.d.ts +8 -0
- package/dist/seo/xml-escape.d.ts +1 -3
- package/dist/seo/xml-escape.js +1 -6
- package/dist/widgets/concurrency.d.ts +27 -1
- package/dist/widgets/concurrency.js +12 -23
- package/dist/widgets/config-validation.js +6 -0
- package/dist/widgets/gallery-resolver.d.ts +23 -0
- package/dist/widgets/gallery-resolver.js +80 -0
- package/dist/widgets/html/gallery-render.d.ts +24 -0
- package/dist/widgets/html/gallery-render.js +65 -0
- package/dist/widgets/html/html-embeds.d.ts +4 -0
- package/dist/widgets/html/html-embeds.js +7 -1
- package/dist/widgets/html/index.d.ts +3 -0
- package/dist/widgets/html/index.js +3 -0
- package/dist/widgets/html/server-fragment.d.ts +9 -0
- package/dist/widgets/html/server-fragment.js +38 -0
- package/dist/widgets/markers/gallery.d.ts +50 -0
- package/dist/widgets/markers/gallery.js +164 -0
- package/dist/widgets/markers/html-attributes.d.ts +6 -1
- package/dist/widgets/markers/html-attributes.js +24 -9
- package/dist/widgets/markers/index.d.ts +2 -0
- package/dist/widgets/markers/index.js +2 -0
- package/dist/widgets/markers/marker.d.ts +4 -0
- package/dist/widgets/markers/marker.js +6 -0
- package/dist/widgets/markers/media-reference.d.ts +11 -0
- package/dist/widgets/markers/media-reference.js +11 -0
- package/dist/widgets/page-embed-types.d.ts +1 -1
- package/dist/widgets/page-embed-types.js +1 -1
- package/dist/widgets/registry.d.ts +144 -10
- package/dist/widgets/registry.js +40 -0
- package/dist/widgets/resolver-service.d.ts +10 -0
- package/dist/widgets/resolver-service.js +73 -29
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +3 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +2 -0
- package/dist/widgets/resolvers/gallery.d.ts +12 -0
- package/dist/widgets/resolvers/gallery.js +33 -0
- package/dist/widgets/resolvers/index.d.ts +1 -0
- package/dist/widgets/resolvers/index.js +1 -0
- package/dist/widgets/resolvers/menu.d.ts +8 -0
- package/dist/widgets/resolvers/menu.js +21 -6
- package/dist/widgets/types.d.ts +3 -1
- package/package.json +51 -6
- package/dist/http/settings/cms-adapter.d.ts +0 -20
- package/dist/http/settings/cms-adapter.js +0 -124
- package/dist/http/settings/contracts.d.ts +0 -135
- package/dist/http/settings/contracts.js +0 -2
- package/dist/http/settings/http.d.ts +0 -10
- package/dist/http/settings/http.js +0 -211
- package/dist/http/settings/index.d.ts +0 -5
- package/dist/http/settings/index.js +0 -4
- package/dist/http/settings/shared.d.ts +0 -68
- package/dist/http/settings/shared.js +0 -54
- package/dist/http/settings/stream.d.ts +0 -24
- package/dist/http/settings/stream.js +0 -147
- package/dist/settings/agent-tools.d.ts +0 -10
- package/dist/settings/agent-tools.js +0 -204
- package/dist/settings/agent-value-write-tools.d.ts +0 -22
- package/dist/settings/agent-value-write-tools.js +0 -154
- package/dist/settings/agent-writable-preferences.d.ts +0 -67
- package/dist/settings/agent-writable-preferences.js +0 -132
- package/dist/settings/agent-write-denylist.d.ts +0 -19
- package/dist/settings/agent-write-denylist.js +0 -15
- package/dist/settings/change-feed.d.ts +0 -83
- package/dist/settings/change-feed.js +0 -61
- package/dist/settings/definitions-dispatch.d.ts +0 -56
- package/dist/settings/definitions-dispatch.js +0 -86
- package/dist/settings/dictionaries/index.d.ts +0 -48
- package/dist/settings/dictionaries/index.js +0 -69
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -27
- package/dist/settings/ensure-definitions.d.ts +0 -88
- package/dist/settings/ensure-definitions.js +0 -91
- package/dist/settings/errors.d.ts +0 -42
- package/dist/settings/errors.js +0 -46
- package/dist/settings/express/cms-adapter.d.ts +0 -20
- package/dist/settings/express/cms-adapter.js +0 -124
- package/dist/settings/express/contracts.d.ts +0 -135
- package/dist/settings/express/contracts.js +0 -2
- package/dist/settings/express/http.d.ts +0 -10
- package/dist/settings/express/http.js +0 -211
- package/dist/settings/express/shared.d.ts +0 -68
- package/dist/settings/express/shared.js +0 -54
- package/dist/settings/express/stream.d.ts +0 -24
- package/dist/settings/express/stream.js +0 -147
- package/dist/settings/ports.d.ts +0 -118
- package/dist/settings/principal-lookup.d.ts +0 -9
- package/dist/settings/principal-lookup.js +0 -2
- package/dist/settings/purge-service.d.ts +0 -54
- package/dist/settings/purge-service.js +0 -106
- package/dist/settings/repo.memory.d.ts +0 -119
- package/dist/settings/repo.memory.js +0 -199
- package/dist/settings/settings.d.ts +0 -139
- package/dist/settings/settings.js +0 -259
- package/dist/settings/tool-registrations.d.ts +0 -51
- package/dist/settings/tool-registrations.js +0 -250
- package/dist/settings/types.d.ts +0 -125
- package/dist/settings/types.js +0 -3
- package/dist/settings/ui-tab-definitions.d.ts +0 -41
- package/dist/settings/ui-tab-definitions.js +0 -148
- package/dist/settings/write-service.d.ts +0 -320
- package/dist/settings/write-service.js +0 -683
- /package/dist/{settings → routing}/ports.js +0 -0
package/dist/seo/media.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mimeForTransformFormat } from "../media/transform-types.js";
|
|
1
2
|
/**
|
|
2
3
|
* @file `resolveSeoImageRef` (ADR-PIPE-008 Decision §6, C-013, EC-07;
|
|
3
4
|
* EC-07's "never generates" clause AMENDED 2026-09-05 — see the ADR's
|
|
@@ -83,23 +84,47 @@ function buildSeoImageUrl(deps, asset, transformName, latest) {
|
|
|
83
84
|
* version listing).
|
|
84
85
|
*/
|
|
85
86
|
export async function resolveSeoImageRef(required, _optional = {}) {
|
|
87
|
+
return (await resolveSeoImageDetails(required))?.url;
|
|
88
|
+
}
|
|
89
|
+
/** Resolve through the existing media owners, optionally selecting a host's share transform.
|
|
90
|
+
* Absolute external URLs remain unchanged with unknown metadata; no remote fetch occurs.
|
|
91
|
+
* The latest definition keeps the anonymous lazy-generation guarantee documented above.
|
|
92
|
+
* @param required Media ports, workspace and persisted reference.
|
|
93
|
+
* @param optional Host-selected transform; omitted or unregistered retains the reference's named transform.
|
|
94
|
+
* @returns URL and known output metadata, or undefined on a lookup miss.
|
|
95
|
+
* @complexity O(v) in registered transform versions; up to three bounded repository reads.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveSeoImageDetails(required, optional = {}) {
|
|
86
98
|
const { deps, input } = required;
|
|
87
99
|
const ref = input.ref?.trim();
|
|
88
100
|
if (!ref)
|
|
89
101
|
return undefined;
|
|
90
102
|
if (isAbsoluteUrl(ref))
|
|
91
|
-
return ref;
|
|
103
|
+
return { url: ref, ...(optional.transformName === undefined ? {} : { ref }) };
|
|
92
104
|
const parts = parseMediaRefParts(ref);
|
|
93
105
|
if (!parts)
|
|
94
106
|
return undefined;
|
|
95
|
-
const
|
|
96
|
-
const asset = await resolveVisibleAsset(deps, input.workspaceId, idOrSlug);
|
|
107
|
+
const asset = await resolveVisibleAsset(deps, input.workspaceId, parts.idOrSlug);
|
|
97
108
|
if (!asset)
|
|
98
109
|
return undefined;
|
|
99
|
-
|
|
110
|
+
let transformName = optional.transformName ?? parts.transformName;
|
|
111
|
+
let latest = await resolveLatestTransformVersion(deps, input.workspaceId, transformName);
|
|
112
|
+
// Share-transform registration may still be in flight at boot. Keep the persisted reference
|
|
113
|
+
// servable through its own transform rather than dropping the image from both social cards.
|
|
114
|
+
if (!latest && transformName !== parts.transformName) {
|
|
115
|
+
transformName = parts.transformName;
|
|
116
|
+
latest = await resolveLatestTransformVersion(deps, input.workspaceId, transformName);
|
|
117
|
+
}
|
|
100
118
|
if (!latest)
|
|
101
119
|
return undefined;
|
|
102
|
-
|
|
120
|
+
// inside/outside fit does not produce the exact declared box, so don't advertise that box.
|
|
121
|
+
const exactBox = latest.params.width !== undefined && latest.params.height !== undefined && latest.params.fit !== "inside" && latest.params.fit !== "outside";
|
|
122
|
+
return {
|
|
123
|
+
url: buildSeoImageUrl(deps, asset, transformName, latest),
|
|
124
|
+
...(optional.transformName === undefined ? {} : { ref }),
|
|
125
|
+
...(exactBox ? { width: latest.params.width, height: latest.params.height } : {}),
|
|
126
|
+
type: mimeForTransformFormat({ format: latest.params.format }),
|
|
127
|
+
};
|
|
103
128
|
}
|
|
104
129
|
/**
|
|
105
130
|
* The write-path refusal for an `ogImage`/`twitterImage`/`defaultOgImage` value (2026-10-05): the
|
package/dist/seo/page-head.js
CHANGED
|
@@ -22,8 +22,15 @@ function buildOpenGraphElements(og) {
|
|
|
22
22
|
{ kind: "og", property: "og:type", content: og.type, priority: 141 },
|
|
23
23
|
{ kind: "og", property: "og:url", content: og.url, priority: 142 },
|
|
24
24
|
];
|
|
25
|
-
if (og.image)
|
|
25
|
+
if (og.image) {
|
|
26
26
|
elements.push({ kind: "og", property: "og:image", content: og.image, priority: 143 });
|
|
27
|
+
if (og.imageWidth !== undefined)
|
|
28
|
+
elements.push({ kind: "og", property: "og:image:width", content: String(og.imageWidth), priority: 143 });
|
|
29
|
+
if (og.imageHeight !== undefined)
|
|
30
|
+
elements.push({ kind: "og", property: "og:image:height", content: String(og.imageHeight), priority: 143 });
|
|
31
|
+
if (og.imageType)
|
|
32
|
+
elements.push({ kind: "og", property: "og:image:type", content: og.imageType, priority: 143 });
|
|
33
|
+
}
|
|
27
34
|
if (og.description)
|
|
28
35
|
elements.push({ kind: "og", property: "og:description", content: og.description, priority: 144 });
|
|
29
36
|
return elements;
|
package/dist/seo/seo.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface GetEntryMetaDeps {
|
|
|
37
37
|
postRepo: SeoPostReadPort;
|
|
38
38
|
settings: SeoSettingsReaderPort;
|
|
39
39
|
media: ResolveSeoImageRefDeps;
|
|
40
|
+
/** Host-owned share-card variant; omitted preserves persisted transform choices. */
|
|
41
|
+
shareImageTransform?: string | undefined;
|
|
40
42
|
originRegistry: OriginRegistryPort;
|
|
41
43
|
runtimeMode: string;
|
|
42
44
|
urls: SeoUrlsPort;
|
package/dist/seo/seo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveWorkspaceOrigin, toAbsoluteUrl } from "./absolute-url.js";
|
|
2
2
|
import { SeoEntryNotFoundError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSeoImageDetails } from "./media.js";
|
|
4
4
|
/** Featured image (2026-10-05) as a share-image ref: the core "public" transform every `/m/` URL
|
|
5
5
|
* already serves (`deps.ts`'s ADR-027 §4 registration). */
|
|
6
6
|
function featuredImageRef(post) {
|
|
@@ -106,7 +106,7 @@ async function resolveRobots(deps, post, overrides, settings, workspaceId) {
|
|
|
106
106
|
/** The first of `refs` that resolves through media, or `undefined`. @complexity O(r) resolves. */
|
|
107
107
|
async function firstResolvedImage(deps, workspaceId, refs) {
|
|
108
108
|
for (const ref of refs) {
|
|
109
|
-
const resolved = ref ? await
|
|
109
|
+
const resolved = ref ? await resolveSeoImageDetails({ deps: deps.media, input: { workspaceId, ref } }, { transformName: deps.shareImageTransform }) : undefined;
|
|
110
110
|
if (resolved)
|
|
111
111
|
return resolved;
|
|
112
112
|
}
|
|
@@ -125,8 +125,8 @@ async function resolveShareImages(deps, post, overrides, settings, workspaceId,
|
|
|
125
125
|
const ogImageResolved = await firstResolvedImage(deps, workspaceId, overrides.ogImage ? [overrides.ogImage] : fallbacks);
|
|
126
126
|
const twitterImageResolved = await firstResolvedImage(deps, workspaceId, overrides.twitterImage ? [overrides.twitterImage] : fallbacks);
|
|
127
127
|
return {
|
|
128
|
-
ogImage: ogImageResolved ? toAbsoluteUrl({ origin, path: ogImageResolved }) : undefined,
|
|
129
|
-
twitterImage: twitterImageResolved ? toAbsoluteUrl({ origin, path: twitterImageResolved }) : undefined,
|
|
128
|
+
ogImage: ogImageResolved ? { ...ogImageResolved, url: toAbsoluteUrl({ origin, path: ogImageResolved.url }) } : undefined,
|
|
129
|
+
twitterImage: twitterImageResolved ? { ...twitterImageResolved, url: toAbsoluteUrl({ origin, path: twitterImageResolved.url }) } : undefined,
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
/** title: override, else titleTemplate applied to entry.title. description: override > site default >
|
|
@@ -150,7 +150,11 @@ function buildOpenGraph(overrides, title, description, ogType, canonical, ogImag
|
|
|
150
150
|
description: overrides.ogDescription ?? description,
|
|
151
151
|
type: ogType,
|
|
152
152
|
url: canonical,
|
|
153
|
-
image: ogImage,
|
|
153
|
+
image: ogImage?.url,
|
|
154
|
+
...(ogImage?.ref === undefined ? {} : { imageRef: ogImage.ref }),
|
|
155
|
+
...(ogImage?.width === undefined ? {} : { imageWidth: ogImage.width }),
|
|
156
|
+
...(ogImage?.height === undefined ? {} : { imageHeight: ogImage.height }),
|
|
157
|
+
...(ogImage?.type === undefined ? {} : { imageType: ogImage.type }),
|
|
154
158
|
siteName: undefined,
|
|
155
159
|
};
|
|
156
160
|
}
|
|
@@ -159,7 +163,8 @@ function buildTwitter(overrides, title, description, twitterImage, settings) {
|
|
|
159
163
|
card: overrides.twitterCard ?? "summary_large_image",
|
|
160
164
|
title: overrides.twitterTitle ?? title,
|
|
161
165
|
description: overrides.twitterDescription ?? description,
|
|
162
|
-
image: twitterImage,
|
|
166
|
+
image: twitterImage?.url,
|
|
167
|
+
...(twitterImage?.ref === undefined ? {} : { imageRef: twitterImage.ref }),
|
|
163
168
|
site: settings.twitterSite,
|
|
164
169
|
};
|
|
165
170
|
}
|
package/dist/seo/types.d.ts
CHANGED
|
@@ -188,6 +188,12 @@ export interface OpenGraph {
|
|
|
188
188
|
type: OpenGraphType;
|
|
189
189
|
url: string;
|
|
190
190
|
image?: string | undefined;
|
|
191
|
+
/** Original selected source for admin diagnostics, before any share-card resize. */
|
|
192
|
+
imageRef?: string | undefined;
|
|
193
|
+
/** Known output metadata from the selected named transform, omitted for external URLs. */
|
|
194
|
+
imageWidth?: number | undefined;
|
|
195
|
+
imageHeight?: number | undefined;
|
|
196
|
+
imageType?: string | undefined;
|
|
191
197
|
siteName?: string | undefined;
|
|
192
198
|
}
|
|
193
199
|
export interface TwitterCard {
|
|
@@ -195,6 +201,8 @@ export interface TwitterCard {
|
|
|
195
201
|
title: string;
|
|
196
202
|
description?: string | undefined;
|
|
197
203
|
image?: string | undefined;
|
|
204
|
+
/** Original selected source for admin diagnostics, before any share-card resize. */
|
|
205
|
+
imageRef?: string | undefined;
|
|
198
206
|
site?: string | undefined;
|
|
199
207
|
}
|
|
200
208
|
export interface RobotsDirective {
|
package/dist/seo/xml-escape.d.ts
CHANGED
|
@@ -10,7 +10,5 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** {@link escapeHtml} with the XML named entity `'` for the apostrophe (sitemap and feed XML).
|
|
12
12
|
* @complexity O(text length). */
|
|
13
|
-
export
|
|
14
|
-
text: string;
|
|
15
|
-
}, _optional?: Record<string, never>): string;
|
|
13
|
+
export { escapeXml } from "@jini-ai/core/text";
|
|
16
14
|
//# sourceMappingURL=xml-escape.d.ts.map
|
package/dist/seo/xml-escape.js
CHANGED
|
@@ -8,12 +8,7 @@
|
|
|
8
8
|
* other character first would double-escape the `&` that replacement introduced. The apostrophe
|
|
9
9
|
* becomes the numeric `'` in HTML, because `'` is undefined in HTML4/XHTML1.
|
|
10
10
|
*/
|
|
11
|
-
const XML_ENTITIES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
12
|
-
const SPECIAL = /[&<>"']/g;
|
|
13
11
|
/** {@link escapeHtml} with the XML named entity `'` for the apostrophe (sitemap and feed XML).
|
|
14
12
|
* @complexity O(text length). */
|
|
15
|
-
|
|
16
|
-
export function escapeXml(required, _optional = {}) {
|
|
17
|
-
return required.text.replace(SPECIAL, (char) => XML_ENTITIES[char] ?? char);
|
|
18
|
-
}
|
|
13
|
+
export { escapeXml } from "@jini-ai/core/text";
|
|
19
14
|
//# sourceMappingURL=xml-escape.js.map
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A tiny per-key async mutex (SPEC-043 REQ-06/15, AC-04/EC-02).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* `features/entries/write-service.ts`'s `updateEntry` reads the current row, checks
|
|
6
|
+
* `expectedVersion`, then writes — but `InMemoryEntryRepo.transaction` provides no real
|
|
7
|
+
* serialization (`async transaction(fn) { return fn(); }`), and the version check itself happens
|
|
8
|
+
* BEFORE that transaction call. Two truly concurrent callers racing the same entry id can both
|
|
9
|
+
* observe the pre-write version and both pass the check under the in-memory adapter (the real
|
|
10
|
+
* `SqliteEntryRepo.transaction`'s `BEGIN IMMEDIATE` already serializes real concurrent writers at
|
|
11
|
+
* the DB layer — this mutex is redundant-but-harmless there, and load-bearing for the in-memory
|
|
12
|
+
* path this test suite's concurrency assertions exercise).
|
|
13
|
+
*
|
|
14
|
+
* This module serializes the whole "read current version -> validate -> call updateEntry" sequence
|
|
15
|
+
* per entry id at the WIDGETS domain layer, restoring correct optimistic-concurrency semantics
|
|
16
|
+
* deterministically regardless of the underlying repo adapter's own concurrency properties.
|
|
17
|
+
*
|
|
18
|
+
* Architectural role:
|
|
19
|
+
* `widgets` domain-internal helper, not a frozen public contract.
|
|
20
|
+
*/
|
|
21
|
+
export type EntryLockState = Map<string, Promise<void>>;
|
|
22
|
+
/** A host can own this state; the compatibility adapter still serializes existing callers.
|
|
23
|
+
* Idle keys are removed by withEntryLock. @complexity O(1). */
|
|
24
|
+
export declare function createEntryLockState(_required: Record<string, never>, _optional?: Record<string, never>): EntryLockState;
|
|
1
25
|
/**
|
|
2
26
|
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
3
27
|
* callers queue in call order, each waiting for the previous holder to finish.
|
|
@@ -8,5 +32,7 @@
|
|
|
8
32
|
export declare function withEntryLock<T>({ key, fn }: {
|
|
9
33
|
key: string;
|
|
10
34
|
fn: () => Promise<T>;
|
|
11
|
-
},
|
|
35
|
+
}, { locks }?: {
|
|
36
|
+
locks?: EntryLockState;
|
|
37
|
+
}): Promise<T>;
|
|
12
38
|
//# sourceMappingURL=concurrency.d.ts.map
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* serialization (`async transaction(fn) { return fn(); }`), and the version check itself happens
|
|
8
|
-
* BEFORE that transaction call. Two truly concurrent callers racing the same entry id can both
|
|
9
|
-
* observe the pre-write version and both pass the check under the in-memory adapter (the real
|
|
10
|
-
* `SqliteEntryRepo.transaction`'s `BEGIN IMMEDIATE` already serializes real concurrent writers at
|
|
11
|
-
* the DB layer — this mutex is redundant-but-harmless there, and load-bearing for the in-memory
|
|
12
|
-
* path this test suite's concurrency assertions exercise).
|
|
13
|
-
*
|
|
14
|
-
* This module serializes the whole "read current version -> validate -> call updateEntry" sequence
|
|
15
|
-
* per entry id at the WIDGETS domain layer, restoring correct optimistic-concurrency semantics
|
|
16
|
-
* deterministically regardless of the underlying repo adapter's own concurrency properties.
|
|
17
|
-
*
|
|
18
|
-
* Architectural role:
|
|
19
|
-
* `widgets` domain-internal helper, not a frozen public contract.
|
|
20
|
-
*/
|
|
21
|
-
const locks = new Map();
|
|
1
|
+
/** A host can own this state; the compatibility adapter still serializes existing callers.
|
|
2
|
+
* Idle keys are removed by withEntryLock. @complexity O(1). */
|
|
3
|
+
export function createEntryLockState(_required, _optional = {}) {
|
|
4
|
+
return new Map();
|
|
5
|
+
}
|
|
6
|
+
const defaultLocks = createEntryLockState({});
|
|
22
7
|
/**
|
|
23
8
|
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
24
9
|
* callers queue in call order, each waiting for the previous holder to finish.
|
|
@@ -26,19 +11,23 @@ const locks = new Map();
|
|
|
26
11
|
* @complexity O(1) scheduling overhead beyond `fn`'s own cost.
|
|
27
12
|
* @overallScore 100
|
|
28
13
|
*/
|
|
29
|
-
export async function withEntryLock({ key, fn },
|
|
14
|
+
export async function withEntryLock({ key, fn }, { locks = defaultLocks } = {}) {
|
|
30
15
|
const previous = locks.get(key) ?? Promise.resolve();
|
|
31
16
|
let release;
|
|
32
17
|
const next = new Promise((resolve) => {
|
|
33
18
|
release = resolve;
|
|
34
19
|
});
|
|
35
|
-
|
|
20
|
+
const tail = previous.then(() => next);
|
|
21
|
+
locks.set(key, tail);
|
|
36
22
|
await previous;
|
|
37
23
|
try {
|
|
38
24
|
return await fn();
|
|
39
25
|
}
|
|
40
26
|
finally {
|
|
41
27
|
release();
|
|
28
|
+
// A newer waiter owns the key until it finishes; deleting its tail would let callers race.
|
|
29
|
+
if (locks.get(key) === tail)
|
|
30
|
+
locks.delete(key);
|
|
42
31
|
}
|
|
43
32
|
}
|
|
44
33
|
//# sourceMappingURL=concurrency.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseGalleryConfig } from "./markers/gallery.js";
|
|
1
2
|
import { isNonArrayRecord } from "@jini-ai/core";
|
|
2
3
|
/** Reports every key in `schema.required` that is missing from `value`. */
|
|
3
4
|
function validateRequiredKeys(value, schema, path, errors) {
|
|
@@ -94,6 +95,11 @@ function walk(value, schema, path, errors) {
|
|
|
94
95
|
export function validateWidgetConfig(required, _optional = {}) {
|
|
95
96
|
const errors = [];
|
|
96
97
|
walk(required.config, required.schema, "config", errors);
|
|
98
|
+
// The shared parser owns layout, id and CSS-ratio invariants; the schema narrows widgets to library images.
|
|
99
|
+
if (required.schema["x-gallery-config"] === true) {
|
|
100
|
+
for (const reason of parseGalleryConfig({ config: required.config }, { allowEmpty: true }).errors)
|
|
101
|
+
errors.push({ field: "config", reason });
|
|
102
|
+
}
|
|
97
103
|
return { valid: errors.length === 0, fieldErrors: errors };
|
|
98
104
|
}
|
|
99
105
|
/** Pushes each declared free-text string under `schema` (recursion into declared `properties` and
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PageHtmlEmbedRef } from './html/html-embeds.js';
|
|
2
|
+
import type { WidgetRenderIR } from './types.js';
|
|
3
|
+
export type GalleryMediaResolver = (required: {
|
|
4
|
+
refs: readonly PageHtmlEmbedRef[];
|
|
5
|
+
}, optional?: Record<string, never>) => Promise<ReadonlyMap<string, WidgetRenderIR>>;
|
|
6
|
+
export type GalleryIssueReporter = (required: {
|
|
7
|
+
key: string | null;
|
|
8
|
+
messages: readonly string[];
|
|
9
|
+
}, optional?: Record<string, never>) => void;
|
|
10
|
+
/** Resolve bounded, deduplicated gallery assets through the existing media resolver port.
|
|
11
|
+
* Occurrence keys preserve different layouts/alt/captions for the same assets on one page.
|
|
12
|
+
* At the page media budget, whole later galleries degrade instead of silently losing images.
|
|
13
|
+
* @complexity O(g*i) time/space for g markers and i<=50 images; one media batch of at most maxMedia.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveGalleryEmbeds({ refs, resolveMedia }: {
|
|
16
|
+
refs: readonly PageHtmlEmbedRef[];
|
|
17
|
+
resolveMedia: GalleryMediaResolver;
|
|
18
|
+
}, { maxMedia, timeoutMs, reportIssue }?: {
|
|
19
|
+
maxMedia?: number;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
reportIssue?: GalleryIssueReporter;
|
|
22
|
+
}): Promise<ReadonlyMap<string, WidgetRenderIR>>;
|
|
23
|
+
//# sourceMappingURL=gallery-resolver.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { galleryMediaReferences, parseGalleryConfig } from './markers/gallery.js';
|
|
2
|
+
/** Bound the media wait without taking ownership of the host repository's cancellation policy.
|
|
3
|
+
* The port has no abort signal; an expired call may finish in the background but is never awaited
|
|
4
|
+
* indefinitely and its timer is always released. No retries or duplicate repository calls.
|
|
5
|
+
*/
|
|
6
|
+
async function resolveGalleryMedia({ resolveMedia, refs, timeoutMs }) {
|
|
7
|
+
let timer;
|
|
8
|
+
try {
|
|
9
|
+
return await Promise.race([
|
|
10
|
+
resolveMedia({ refs }),
|
|
11
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(undefined), timeoutMs); }),
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Resolve bounded, deduplicated gallery assets through the existing media resolver port.
|
|
19
|
+
* Occurrence keys preserve different layouts/alt/captions for the same assets on one page.
|
|
20
|
+
* At the page media budget, whole later galleries degrade instead of silently losing images.
|
|
21
|
+
* @complexity O(g*i) time/space for g markers and i<=50 images; one media batch of at most maxMedia.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveGalleryEmbeds({ refs, resolveMedia }, { maxMedia = 50, timeoutMs = 10000, reportIssue } = {}) {
|
|
24
|
+
const accepted = [];
|
|
25
|
+
const legacyIds = new Set();
|
|
26
|
+
const slugKeys = new Set();
|
|
27
|
+
const mediaRefs = new Map();
|
|
28
|
+
for (const ref of refs) {
|
|
29
|
+
const config = ref.config?.content ?? ref.config;
|
|
30
|
+
const parsed = parseGalleryConfig({ config }, { allowEmpty: ref.type === 'media' });
|
|
31
|
+
if (!parsed.config || ref.id === null) {
|
|
32
|
+
reportIssue?.({ key: ref.id, messages: parsed.errors });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const ids = new Set(parsed.config.mode === 'html'
|
|
36
|
+
? galleryMediaReferences({ config: { html: parsed.config.html } }).map(item => item.mediaId)
|
|
37
|
+
: parsed.config.images.flatMap(item => (item.slug ?? item.mediaId) === undefined ? [] : [(item.slug ?? item.mediaId)]));
|
|
38
|
+
const added = [...ids].filter(id => !mediaRefs.has(id));
|
|
39
|
+
if (mediaRefs.size + added.length > maxMedia) {
|
|
40
|
+
reportIssue?.({ key: ref.id, messages: [`gallery media exceeds the page budget of ${maxMedia} distinct assets`] });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
accepted.push({ ...ref, config: config });
|
|
44
|
+
const legacy = parsed.config.mode === undefined;
|
|
45
|
+
for (const key of ids)
|
|
46
|
+
(legacy ? legacyIds : slugKeys).add(key);
|
|
47
|
+
for (const id of added)
|
|
48
|
+
mediaRefs.set(id, { type: 'media', id: legacy ? id : null, slug: legacy ? null : id, name: null, variant: null, header: true });
|
|
49
|
+
}
|
|
50
|
+
// No second repository lifecycle: media owns workspace lookup, visibility, transforms and metadata.
|
|
51
|
+
const media = mediaRefs.size === 0 ? new Map() : await resolveGalleryMedia({ resolveMedia, refs: [...mediaRefs.values()], timeoutMs });
|
|
52
|
+
if (media === undefined) {
|
|
53
|
+
reportIssue?.({ key: null, messages: ['gallery media resolution timed out'] });
|
|
54
|
+
return new Map();
|
|
55
|
+
}
|
|
56
|
+
// Author-supplied ids are map keys, including names such as __proto__; never assign them onto
|
|
57
|
+
// an object with a prototype or inherit a fake media record from Object.prototype.
|
|
58
|
+
const imageProps = Object.create(null);
|
|
59
|
+
for (const [id, ir] of media) {
|
|
60
|
+
// Legacy mediaId is an indexed asset id, not the single-media marker's slug alias. The reused
|
|
61
|
+
// media resolver accepts both; accepting a slug here would render an asset that the pure
|
|
62
|
+
// entry_refs extractor cannot protect, since it cannot resolve a slug to the canonical id.
|
|
63
|
+
// New slug content deliberately indexes authored keys; the host usage adapter reads id and slug.
|
|
64
|
+
if (legacyIds.has(id) && !slugKeys.has(id)) {
|
|
65
|
+
if (ir.props.assetId !== id) {
|
|
66
|
+
reportIssue?.({ key: id, messages: ['gallery mediaId must be the canonical media id, not a slug'] });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// A video resolved by the generic media pipeline is not an image in a legacy gallery.
|
|
70
|
+
if (typeof ir.props.contentType === 'string' && (ir.props.contentType.startsWith('video/') || ir.props.contentType.startsWith('audio/')))
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
imageProps[id] = ir.props;
|
|
74
|
+
}
|
|
75
|
+
for (const key of slugKeys)
|
|
76
|
+
if (!media.has(key))
|
|
77
|
+
reportIssue?.({ key, messages: [`unknown media slug: ${key}`] });
|
|
78
|
+
return new Map(accepted.map(ref => [ref.id, { componentId: ref.type === 'media' ? 'media' : 'gallery', props: { config: ref.config, media: imageProps } }]));
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=gallery-resolver.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { JsonObject } from '@jini-ai/core/primitives';
|
|
2
|
+
import { type GalleryImage } from '../markers/gallery.js';
|
|
3
|
+
/** Layout-only baseline; host themes override jini classes/custom properties without palette coupling. */
|
|
4
|
+
export declare const GALLERY_STYLE = "<style data-jini-gallery-style>\n.jini-gallery{gap:var(--jini-gallery-gap,1rem)}\n.jini-gallery-item{margin:0;min-width:0;break-inside:avoid}\n.jini-gallery audio{display:block;width:100%}\n.jini-gallery img,.jini-gallery video{display:block;width:100%;height:auto;aspect-ratio:var(--jini-gallery-ratio,auto);object-fit:cover}\n.jini-gallery-single{display:grid;grid-template-columns:1fr}\n.jini-gallery-grid{display:grid;grid-template-columns:repeat(var(--jini-gallery-columns,3),minmax(0,1fr))}\n.jini-gallery-masonry{columns:var(--jini-gallery-columns,3);column-gap:var(--jini-gallery-gap,1rem)}\n.jini-gallery-masonry .jini-gallery-item{margin-block-end:var(--jini-gallery-gap,1rem)}\n.jini-gallery-carousel{display:flex;overflow-x:auto;scroll-snap-type:x mandatory}\n.jini-gallery-carousel .jini-gallery-item{flex:0 0 min(85%,36rem);scroll-snap-align:start}\n@media(max-width:48rem){.jini-gallery-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.jini-gallery-masonry{columns:2}}\n@media(max-width:30rem){.jini-gallery-grid{grid-template-columns:1fr}.jini-gallery-masonry{columns:1}}\n</style>";
|
|
5
|
+
export type GalleryImageRenderer = (required: {
|
|
6
|
+
image: GalleryImage & {
|
|
7
|
+
alt: string;
|
|
8
|
+
};
|
|
9
|
+
media: JsonObject | undefined;
|
|
10
|
+
}, optional?: Record<string, never>) => string;
|
|
11
|
+
/** Server-render accessible gallery markup. Image URLs, metadata and srcset belong to the host
|
|
12
|
+
* image pipeline supplied as a port, never templated here. Invalid config produces no output.
|
|
13
|
+
* @complexity O(n) time and output space for at most 50 images, without I/O.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderGallery({ config, media, renderImage, renderRawHtml }: {
|
|
16
|
+
config: unknown;
|
|
17
|
+
media?: unknown;
|
|
18
|
+
renderImage: GalleryImageRenderer;
|
|
19
|
+
renderRawHtml?: (required: {
|
|
20
|
+
html: string;
|
|
21
|
+
media: Record<string, JsonObject>;
|
|
22
|
+
}, optional?: Record<string, never>) => string;
|
|
23
|
+
}, _optional?: Record<string, never>): string;
|
|
24
|
+
//# sourceMappingURL=gallery-render.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { escapeHtml } from '@jini-ai/core/text';
|
|
2
|
+
import { parseEmbedHtmlAttributes } from '../markers/html-attributes.js';
|
|
3
|
+
import { parseGalleryConfig } from '../markers/gallery.js';
|
|
4
|
+
/** Layout-only baseline; host themes override jini classes/custom properties without palette coupling. */
|
|
5
|
+
export const GALLERY_STYLE = `<style data-jini-gallery-style>
|
|
6
|
+
.jini-gallery{gap:var(--jini-gallery-gap,1rem)}
|
|
7
|
+
.jini-gallery-item{margin:0;min-width:0;break-inside:avoid}
|
|
8
|
+
.jini-gallery audio{display:block;width:100%}
|
|
9
|
+
.jini-gallery img,.jini-gallery video{display:block;width:100%;height:auto;aspect-ratio:var(--jini-gallery-ratio,auto);object-fit:cover}
|
|
10
|
+
.jini-gallery-single{display:grid;grid-template-columns:1fr}
|
|
11
|
+
.jini-gallery-grid{display:grid;grid-template-columns:repeat(var(--jini-gallery-columns,3),minmax(0,1fr))}
|
|
12
|
+
.jini-gallery-masonry{columns:var(--jini-gallery-columns,3);column-gap:var(--jini-gallery-gap,1rem)}
|
|
13
|
+
.jini-gallery-masonry .jini-gallery-item{margin-block-end:var(--jini-gallery-gap,1rem)}
|
|
14
|
+
.jini-gallery-carousel{display:flex;overflow-x:auto;scroll-snap-type:x mandatory}
|
|
15
|
+
.jini-gallery-carousel .jini-gallery-item{flex:0 0 min(85%,36rem);scroll-snap-align:start}
|
|
16
|
+
@media(max-width:48rem){.jini-gallery-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.jini-gallery-masonry{columns:2}}
|
|
17
|
+
@media(max-width:30rem){.jini-gallery-grid{grid-template-columns:1fr}.jini-gallery-masonry{columns:1}}
|
|
18
|
+
</style>`;
|
|
19
|
+
/** Render one ordered figure using the host's established image renderer and alt fallback.
|
|
20
|
+
* Missing alt is flagged in markup as well as validation; an explicit empty alt stays decorative.
|
|
21
|
+
*/
|
|
22
|
+
function renderFigure({ image, media, renderImage }) {
|
|
23
|
+
if ((image.slug !== undefined || image.mediaId !== undefined) && !media)
|
|
24
|
+
return '';
|
|
25
|
+
const storedAlt = typeof media?.alt === 'string' && media.alt.length > 0 ? media.alt : undefined;
|
|
26
|
+
const alt = image.alt ?? storedAlt;
|
|
27
|
+
const missingAlt = alt === undefined ? ' data-jini-alt-missing="true"' : '';
|
|
28
|
+
const markup = renderImage({ image: { ...image, alt: alt ?? '' }, media });
|
|
29
|
+
const caption = image.caption === undefined ? '' : `<figcaption>${escapeHtml({ text: image.caption })}</figcaption>`;
|
|
30
|
+
return `<figure class="jini-gallery-item"${missingAlt}>${markup}${caption}</figure>`;
|
|
31
|
+
}
|
|
32
|
+
/** Server-render accessible gallery markup. Image URLs, metadata and srcset belong to the host
|
|
33
|
+
* image pipeline supplied as a port, never templated here. Invalid config produces no output.
|
|
34
|
+
* @complexity O(n) time and output space for at most 50 images, without I/O.
|
|
35
|
+
*/
|
|
36
|
+
export function renderGallery({ config, media, renderImage, renderRawHtml }, _optional = {}) {
|
|
37
|
+
const parsed = parseGalleryConfig({ config }, { allowEmpty: true });
|
|
38
|
+
if (!parsed.config)
|
|
39
|
+
return '';
|
|
40
|
+
const gallery = parsed.config;
|
|
41
|
+
const records = typeof media === 'object' && media !== null && !Array.isArray(media) ? media : {};
|
|
42
|
+
if (gallery.mode === 'html')
|
|
43
|
+
return renderRawHtml?.({ html: gallery.html ?? '', media: records }) ?? '';
|
|
44
|
+
// Media-mode configs come from the Media widget editor; naming the Gallery editor there misleads.
|
|
45
|
+
if (gallery.images.length === 0)
|
|
46
|
+
return `<div class="jini-gallery jini-gallery-empty" role="note">${gallery.mode === 'media' ? 'Choose media in the Media widget editor.' : 'Choose images in the Gallery widget editor.'}</div>`;
|
|
47
|
+
const figures = gallery.images.map(image => renderFigure({ image, media: (image.slug ?? image.mediaId) !== undefined && Object.hasOwn(records, (image.slug ?? image.mediaId)) ? records[(image.slug ?? image.mediaId)] : undefined, renderImage })).join('');
|
|
48
|
+
const ratio = gallery.aspectRatio === undefined ? '' : `;--jini-gallery-ratio:${gallery.aspectRatio}`;
|
|
49
|
+
// TODO: bind data-jini-lightbox through a host theme's lightbox script slot when one exists.
|
|
50
|
+
// Until then the gallery stays readable and keyboard-scrollable without any client script.
|
|
51
|
+
const attributes = {
|
|
52
|
+
role: 'region', 'aria-label': gallery.mode === 'media' ? 'Media' : 'Image gallery',
|
|
53
|
+
...(gallery.layout === 'carousel' ? { tabindex: '0', 'aria-roledescription': 'carousel' } : {}),
|
|
54
|
+
...(gallery.lightbox === true ? { 'data-jini-lightbox': 'true' } : {}),
|
|
55
|
+
...parseEmbedHtmlAttributes({ text: gallery.htmlAttributes ?? '' }).attributes,
|
|
56
|
+
};
|
|
57
|
+
const extraClass = [gallery.cssClass, attributes.class].filter(Boolean).join(' ');
|
|
58
|
+
delete attributes.class;
|
|
59
|
+
// Layout styles remain the base; authored safe style declarations override them in order.
|
|
60
|
+
const authoredStyle = attributes.style;
|
|
61
|
+
delete attributes.style;
|
|
62
|
+
const extra = Object.entries(attributes).map(([name, value]) => ` ${name}="${escapeHtml({ text: value })}"`).join('');
|
|
63
|
+
return GALLERY_STYLE + `<div class="jini-gallery jini-gallery-${gallery.layout}${extraClass ? ` ${escapeHtml({ text: extraClass })}` : ''}"${extra} style="--jini-gallery-columns:${gallery.columns ?? 3}${ratio}${authoredStyle ? `;${escapeHtml({ text: authoredStyle })}` : ''}">${figures}</div>`;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=gallery-render.js.map
|
|
@@ -107,6 +107,8 @@ import { type MarkerAttribute } from "../markers/marker.js";
|
|
|
107
107
|
* — this interface is the compatibility seam, not a ceiling.
|
|
108
108
|
*/
|
|
109
109
|
export interface PageHtmlEmbedRef {
|
|
110
|
+
/** Occurrence identity for placement overrides sharing one saved widget. */
|
|
111
|
+
readonly placementKey?: string;
|
|
110
112
|
readonly type: string;
|
|
111
113
|
readonly id: string | null;
|
|
112
114
|
/** See this file's header for why this is NOT the same mechanism as `name` below, despite the
|
|
@@ -129,6 +131,8 @@ export interface PageHtmlEmbedRef {
|
|
|
129
131
|
*/
|
|
130
132
|
readonly header: boolean;
|
|
131
133
|
readonly mode?: "html";
|
|
134
|
+
/** Full inline configuration for compound embeds; existing scalar projections remain stable. */
|
|
135
|
+
readonly config?: Readonly<Record<string, unknown>>;
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
134
138
|
* Resource bound (Programmer workflow step 5a4) mirroring `embed-service.ts`'s
|
|
@@ -156,12 +156,18 @@ function configBooleanDefaultTrue(config, key) {
|
|
|
156
156
|
function toEmbedRef(marker) {
|
|
157
157
|
return {
|
|
158
158
|
type: marker.type.toLowerCase(),
|
|
159
|
-
|
|
159
|
+
// Only saved widget overlays use placement keys. Inline Media content already has its own
|
|
160
|
+
// occurrence id below; a second key would make rendering miss the resolver's media:<n> result.
|
|
161
|
+
...(marker.type.toLowerCase() === "widget" && marker.config.content !== undefined ? { placementKey: `placement:${marker.occurrence}` } : {}),
|
|
162
|
+
// Inline galleries have no target entity. Key by occurrence so repeated assets with different
|
|
163
|
+
// layouts/captions cannot overwrite each other's resolved IR, even with an authored id.
|
|
164
|
+
id: (marker.type.toLowerCase() === "gallery" || (marker.type.toLowerCase() === "media" && (marker.config.content !== undefined || marker.config.items !== undefined || marker.config.html !== undefined))) ? `${marker.type.toLowerCase()}:${marker.occurrence}` : normalizeEmbedId(configString(marker.config, "id")),
|
|
160
165
|
slug: normalizeEmbedSlug(configString(marker.config, "slug")),
|
|
161
166
|
name: configString(marker.config, "name"),
|
|
162
167
|
variant: configString(marker.config, "variant"),
|
|
163
168
|
header: configBooleanDefaultTrue(marker.config, "header"),
|
|
164
169
|
...(marker.config.mode === "html" ? { mode: "html" } : {}),
|
|
170
|
+
config: marker.config,
|
|
165
171
|
};
|
|
166
172
|
}
|
|
167
173
|
/**
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export { resolveGalleryWidgetMedia } from "../resolver-service.js";
|
|
1
2
|
export * from "./html-embeds.js";
|
|
2
3
|
export * from "./legacy-widget-markers.js";
|
|
4
|
+
export * from "./gallery-render.js";
|
|
3
5
|
export * from "./resolve-html-page-embeds.js";
|
|
4
6
|
export type { ResolveHtmlPageEmbedsDeps } from "../resolver-service.js";
|
|
7
|
+
export * from './server-fragment.js';
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
export { resolveGalleryWidgetMedia } from "../resolver-service.js";
|
|
1
2
|
export * from "./html-embeds.js";
|
|
2
3
|
export * from "./legacy-widget-markers.js";
|
|
4
|
+
export * from "./gallery-render.js";
|
|
3
5
|
export * from "./resolve-html-page-embeds.js";
|
|
6
|
+
export * from './server-fragment.js';
|
|
4
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Reject executable/unsupported markup rather than quietly preserving a plugin's unsafe output.
|
|
2
|
+
* URLs are local or HTTP(S); form submission remains native. No inline style, events, SVG,
|
|
3
|
+
* iframe, script, srcdoc, data URLs, or enhancement assets are admitted. */
|
|
4
|
+
export declare function validateServerWidgetFragment(required: {
|
|
5
|
+
html: string;
|
|
6
|
+
}, optional?: {
|
|
7
|
+
maxBytes?: number;
|
|
8
|
+
}): string;
|
|
9
|
+
//# sourceMappingURL=server-fragment.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Public plugin fragments are HTML, never same-origin script (ADR-025). Use the existing
|
|
2
|
+
* HTML parser, not regex sanitization: decoded attributes and foreign namespaces matter. */
|
|
3
|
+
import { parseFragment, serialize } from 'parse5';
|
|
4
|
+
const ELEMENTS = new Set(['div', 'span', 'p', 'br', 'hr', 'strong', 'em', 'b', 'i', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'blockquote', 'pre', 'code', 'a', 'img', 'figure', 'figcaption', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'section', 'article', 'aside', 'header', 'footer', 'nav', 'form', 'label', 'input', 'textarea', 'select', 'option', 'button']);
|
|
5
|
+
const ATTRIBUTES = new Set(['id', 'class', 'title', 'role', 'href', 'src', 'alt', 'width', 'height', 'action', 'method', 'name', 'value', 'type', 'required', 'disabled', 'checked', 'selected', 'multiple', 'placeholder', 'for', 'rows', 'cols', 'min', 'max', 'step', 'colspan', 'rowspan']);
|
|
6
|
+
/** Reject executable/unsupported markup rather than quietly preserving a plugin's unsafe output.
|
|
7
|
+
* URLs are local or HTTP(S); form submission remains native. No inline style, events, SVG,
|
|
8
|
+
* iframe, script, srcdoc, data URLs, or enhancement assets are admitted. */
|
|
9
|
+
export function validateServerWidgetFragment(required, optional = {}) {
|
|
10
|
+
if (new TextEncoder().encode(required.html).byteLength > (optional.maxBytes ?? 65536))
|
|
11
|
+
throw new Error('widget HTML exceeds its size cap');
|
|
12
|
+
const root = parseFragment(required.html);
|
|
13
|
+
const visit = (node) => {
|
|
14
|
+
if ('tagName' in node) {
|
|
15
|
+
if (node.namespaceURI !== 'http://www.w3.org/1999/xhtml' || !ELEMENTS.has(node.tagName))
|
|
16
|
+
throw new Error('executable or unsupported widget element');
|
|
17
|
+
for (const attr of node.attrs) {
|
|
18
|
+
if (attr.namespace || (!ATTRIBUTES.has(attr.name) && !/^aria-[a-z-]+$/.test(attr.name)))
|
|
19
|
+
throw new Error('executable or unsupported widget attribute');
|
|
20
|
+
if (['href', 'src', 'action'].includes(attr.name)) {
|
|
21
|
+
// URL parsing catches entity-decoded schemes and embedded tab/newline obfuscation.
|
|
22
|
+
const value = attr.value;
|
|
23
|
+
if (/[\u0000-\u0020\\]/.test(value) || value.startsWith('//'))
|
|
24
|
+
throw new Error('unsafe widget URL');
|
|
25
|
+
const url = new URL(value, 'https://widget.invalid/');
|
|
26
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
27
|
+
throw new Error('unsafe widget URL');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const child of node.childNodes)
|
|
31
|
+
visit(child);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
for (const node of root.childNodes)
|
|
35
|
+
visit(node);
|
|
36
|
+
return serialize(root);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=server-fragment.js.map
|