@jini-ai/cms 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- 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 +5 -3
- package/dist/media/media-service.js +10 -26
- package/dist/media/node.d.ts +1 -1
- package/dist/media/node.js +1 -1
- package/dist/media/slug.d.ts +8 -0
- package/dist/media/slug.js +32 -0
- 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/dropdown.d.ts +41 -0
- package/dist/navigation/dropdown.js +85 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +2 -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/ports.js +2 -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/types.d.ts +3 -1
- package/package.json +100 -7
|
@@ -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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Inline gallery configuration; media resolution remains owned by the media embed pipeline. */
|
|
2
|
+
export interface GalleryImage {
|
|
3
|
+
readonly mediaId?: string;
|
|
4
|
+
readonly slug?: string;
|
|
5
|
+
readonly src?: string;
|
|
6
|
+
readonly alt?: string;
|
|
7
|
+
readonly caption?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GalleryConfig {
|
|
10
|
+
readonly images: readonly GalleryImage[];
|
|
11
|
+
readonly layout: 'single' | 'grid' | 'carousel' | 'masonry';
|
|
12
|
+
readonly mode?: 'media' | 'html';
|
|
13
|
+
readonly html?: string;
|
|
14
|
+
readonly htmlAttributes?: string;
|
|
15
|
+
readonly cssClass?: string;
|
|
16
|
+
readonly columns?: number;
|
|
17
|
+
readonly aspectRatio?: string;
|
|
18
|
+
readonly lightbox?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const MAX_GALLERY_IMAGES = 50;
|
|
21
|
+
/** Pure validation result, with missing-alt warnings separate from fatal configuration errors.
|
|
22
|
+
* Explicit alt="" denotes decorative content. Unknown keys remain available to host authoring.
|
|
23
|
+
* @complexity O(n) time and space for at most 50 images; oversized arrays are rejected before walking.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseGalleryConfig({ config }: {
|
|
26
|
+
config: unknown;
|
|
27
|
+
}, { allowEmpty }?: {
|
|
28
|
+
allowEmpty?: boolean;
|
|
29
|
+
}): {
|
|
30
|
+
config?: GalleryConfig;
|
|
31
|
+
errors: string[];
|
|
32
|
+
warnings: string[];
|
|
33
|
+
};
|
|
34
|
+
/** References describe authored intent even when another config field is invalid. Do not gate
|
|
35
|
+
* safe-delete or export on successful rendering: that would hide the broken page needing protection.
|
|
36
|
+
* Images beyond the renderable image cap cannot render and do not expand the reference index.
|
|
37
|
+
* @complexity O(min(n,50)) time and space over the authored image list; no I/O or resolution.
|
|
38
|
+
*/
|
|
39
|
+
export declare function galleryMediaReferences({ config }: {
|
|
40
|
+
config: unknown;
|
|
41
|
+
}, _optional?: Record<string, never>): readonly {
|
|
42
|
+
mediaId: string;
|
|
43
|
+
index: number;
|
|
44
|
+
}[];
|
|
45
|
+
/** Authored slug references, independent of render validity. Tokens and quoted data attributes
|
|
46
|
+
* share the media reference key rules. Marker values are already entity-decoded by the scanner. */
|
|
47
|
+
export declare function mediaHtmlSlugs({ html }: {
|
|
48
|
+
html: string;
|
|
49
|
+
}, _optional?: Record<string, never>): readonly string[];
|
|
50
|
+
//# sourceMappingURL=gallery.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { decodeMarkerHtmlAttribute } from './marker.js';
|
|
2
|
+
import { isPlausibleMediaRefId } from './media-reference.js';
|
|
3
|
+
export const MAX_GALLERY_IMAGES = 50;
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/** Same bounded, single-path-segment contract the media resolver accepts. No I/O. */
|
|
8
|
+
function validMediaId(value) {
|
|
9
|
+
return isPlausibleMediaRefId({ value });
|
|
10
|
+
}
|
|
11
|
+
/** Native URL parsing validates browser-loaded sources; hosts may enforce stricter URL policy. */
|
|
12
|
+
function validSource(value) {
|
|
13
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 2048)
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Numeric CSS ratio only: prevents configuration text becoming arbitrary CSS declarations. */
|
|
24
|
+
function validRatio(value) {
|
|
25
|
+
if (typeof value !== 'string' || !/^\d+(?:\.\d+)?(?:\s*\/\s*\d+(?:\.\d+)?)?$/.test(value))
|
|
26
|
+
return false;
|
|
27
|
+
return value.split('/').every(part => Number.isFinite(Number(part)) && Number(part) > 0);
|
|
28
|
+
}
|
|
29
|
+
/** Validate one image independently so a bad sibling never hides another image's diagnostics. */
|
|
30
|
+
function imageErrors(value, index) {
|
|
31
|
+
const path = `images[${index}]`;
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return [`${path} must be an object`];
|
|
34
|
+
const errors = [];
|
|
35
|
+
if ((value.mediaId !== undefined) === (value.src !== undefined))
|
|
36
|
+
errors.push(`${path} requires exactly one of mediaId or src`);
|
|
37
|
+
if (value.mediaId !== undefined && !validMediaId(value.mediaId))
|
|
38
|
+
errors.push(`${path}.mediaId must be a nonempty media id of at most 200 characters, without slashes or whitespace`);
|
|
39
|
+
if (value.src !== undefined && !validSource(value.src))
|
|
40
|
+
errors.push(`${path}.src must be an HTTP(S) image URL without credentials, at most 2048 characters`);
|
|
41
|
+
for (const key of ['alt', 'caption']) {
|
|
42
|
+
if (value[key] !== undefined && typeof value[key] !== 'string')
|
|
43
|
+
errors.push(`${path}.${key} must be a string`);
|
|
44
|
+
}
|
|
45
|
+
return errors;
|
|
46
|
+
}
|
|
47
|
+
/** Pure validation result, with missing-alt warnings separate from fatal configuration errors.
|
|
48
|
+
* Explicit alt="" denotes decorative content. Unknown keys remain available to host authoring.
|
|
49
|
+
* @complexity O(n) time and space for at most 50 images; oversized arrays are rejected before walking.
|
|
50
|
+
*/
|
|
51
|
+
export function parseGalleryConfig({ config }, { allowEmpty = false } = {}) {
|
|
52
|
+
if (!isRecord(config))
|
|
53
|
+
return { errors: ['gallery config must be an object'], warnings: [] };
|
|
54
|
+
if (config.mode !== undefined || config.items !== undefined || config.html !== undefined)
|
|
55
|
+
return parseMediaContent({ config });
|
|
56
|
+
if (!Array.isArray(config.images) || (!allowEmpty && config.images.length === 0) || config.images.length > MAX_GALLERY_IMAGES) {
|
|
57
|
+
return { errors: [`images must contain between 1 and ${MAX_GALLERY_IMAGES} images`], warnings: [] };
|
|
58
|
+
}
|
|
59
|
+
const errors = config.images.flatMap(imageErrors);
|
|
60
|
+
const warnings = config.images.flatMap((image, index) => isRecord(image) && image.alt === undefined
|
|
61
|
+
? [`images[${index}].alt is missing; supply alt text or an explicit empty string for a decorative image`] : []);
|
|
62
|
+
if (typeof config.layout !== 'string' || !['grid', 'carousel', 'masonry'].includes(config.layout))
|
|
63
|
+
errors.push('layout must be grid, carousel or masonry');
|
|
64
|
+
if (config.columns !== undefined && (!Number.isInteger(config.columns) || Number(config.columns) < 2 || Number(config.columns) > 6))
|
|
65
|
+
errors.push('columns must be an integer between 2 and 6');
|
|
66
|
+
if (config.aspectRatio !== undefined && !validRatio(config.aspectRatio))
|
|
67
|
+
errors.push('aspectRatio must be a positive number or ratio such as 16/9');
|
|
68
|
+
if (config.lightbox !== undefined && typeof config.lightbox !== 'boolean')
|
|
69
|
+
errors.push('lightbox must be a boolean');
|
|
70
|
+
if (errors.length > 0)
|
|
71
|
+
return { errors, warnings };
|
|
72
|
+
return { config: config, errors, warnings };
|
|
73
|
+
}
|
|
74
|
+
/** References describe authored intent even when another config field is invalid. Do not gate
|
|
75
|
+
* safe-delete or export on successful rendering: that would hide the broken page needing protection.
|
|
76
|
+
* Images beyond the renderable image cap cannot render and do not expand the reference index.
|
|
77
|
+
* @complexity O(min(n,50)) time and space over the authored image list; no I/O or resolution.
|
|
78
|
+
*/
|
|
79
|
+
export function galleryMediaReferences({ config }, _optional = {}) {
|
|
80
|
+
if (!isRecord(config))
|
|
81
|
+
return [];
|
|
82
|
+
const refs = [];
|
|
83
|
+
const items = Array.isArray(config.items) ? config.items : Array.isArray(config.images) ? config.images : [];
|
|
84
|
+
for (const [index, item] of items.slice(0, MAX_GALLERY_IMAGES).entries()) {
|
|
85
|
+
if (!isRecord(item))
|
|
86
|
+
continue;
|
|
87
|
+
const key = item.slug ?? item.mediaId;
|
|
88
|
+
if (validMediaId(key))
|
|
89
|
+
refs.push({ mediaId: key, index });
|
|
90
|
+
}
|
|
91
|
+
if (typeof config.html === 'string') {
|
|
92
|
+
for (const key of mediaHtmlSlugs({ html: config.html }))
|
|
93
|
+
refs.push({ mediaId: key, index: refs.length });
|
|
94
|
+
}
|
|
95
|
+
if (isRecord(config.content))
|
|
96
|
+
refs.push(...galleryMediaReferences({ config: config.content }));
|
|
97
|
+
return refs;
|
|
98
|
+
}
|
|
99
|
+
/** Normalize the new slug-first content into the existing gallery render contract. Legacy images
|
|
100
|
+
* remain read-only compatibility input; authored media items never accept raw URLs. O(n), n<=50. */
|
|
101
|
+
function parseMediaContent({ config }) {
|
|
102
|
+
const errors = [];
|
|
103
|
+
const mode = config.mode ?? (config.html !== undefined ? 'html' : 'media');
|
|
104
|
+
if (mode !== 'media' && mode !== 'html')
|
|
105
|
+
errors.push('mode must be media or html');
|
|
106
|
+
for (const key of ['htmlAttributes', 'cssClass'])
|
|
107
|
+
if (config[key] !== undefined && typeof config[key] !== 'string')
|
|
108
|
+
errors.push(`${key} must be a string`);
|
|
109
|
+
if (mode === 'html') {
|
|
110
|
+
if (typeof config.html !== 'string' || config.html.length > 65536)
|
|
111
|
+
errors.push('html must be a string of at most 65536 characters');
|
|
112
|
+
return errors.length ? { errors, warnings: [] } : { config: { ...config, mode, images: [], layout: 'single' }, errors, warnings: [] };
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(config.items) || config.items.length > MAX_GALLERY_IMAGES)
|
|
115
|
+
return { errors: ['items must be an array of at most 50 slug items'], warnings: [] };
|
|
116
|
+
const images = [];
|
|
117
|
+
config.items.forEach((item, index) => {
|
|
118
|
+
if (!isRecord(item)) {
|
|
119
|
+
errors.push(`items[${index}] must be an object`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!validMediaId(item.slug))
|
|
123
|
+
errors.push(`items[${index}].slug must be a nonempty slug of at most 200 characters without slashes or whitespace`);
|
|
124
|
+
if (item.src !== undefined || item.mediaId !== undefined)
|
|
125
|
+
errors.push(`items[${index}] uses slug, never src or mediaId`);
|
|
126
|
+
for (const key of ['alt', 'caption'])
|
|
127
|
+
if (item[key] !== undefined && typeof item[key] !== 'string')
|
|
128
|
+
errors.push(`items[${index}].${key} must be a string`);
|
|
129
|
+
images.push(item);
|
|
130
|
+
});
|
|
131
|
+
const requestedLayout = config.layout ?? 'single';
|
|
132
|
+
let layout = 'single';
|
|
133
|
+
if (requestedLayout === 'single' || requestedLayout === 'grid' || requestedLayout === 'carousel' || requestedLayout === 'masonry') {
|
|
134
|
+
layout = requestedLayout;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
errors.push('layout must be single, grid, carousel or masonry');
|
|
138
|
+
}
|
|
139
|
+
if (config.columns !== undefined && (!Number.isInteger(config.columns) || Number(config.columns) < 1 || Number(config.columns) > 6))
|
|
140
|
+
errors.push('columns must be an integer between 1 and 6');
|
|
141
|
+
if (config.aspectRatio !== undefined && !validRatio(config.aspectRatio))
|
|
142
|
+
errors.push('aspectRatio must be a positive number or ratio such as 16/9');
|
|
143
|
+
if (errors.length)
|
|
144
|
+
return { errors, warnings: [] };
|
|
145
|
+
const normalized = { ...config, mode: 'media', images, layout };
|
|
146
|
+
return { config: normalized, errors, warnings: [] };
|
|
147
|
+
}
|
|
148
|
+
/** Authored slug references, independent of render validity. Tokens and quoted data attributes
|
|
149
|
+
* share the media reference key rules. Marker values are already entity-decoded by the scanner. */
|
|
150
|
+
export function mediaHtmlSlugs({ html }, _optional = {}) {
|
|
151
|
+
const keys = [];
|
|
152
|
+
for (const match of html.matchAll(/\{\{media:([^{}]+)\}\}/g)) {
|
|
153
|
+
const key = decodeMarkerHtmlAttribute({ value: match[1] });
|
|
154
|
+
if (validMediaId(key))
|
|
155
|
+
keys.push(key);
|
|
156
|
+
}
|
|
157
|
+
for (const match of html.matchAll(/\bdata-media-slug\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) {
|
|
158
|
+
const key = decodeMarkerHtmlAttribute({ value: match[1] ?? match[2] ?? match[3] ?? '' });
|
|
159
|
+
if (validMediaId(key))
|
|
160
|
+
keys.push(key);
|
|
161
|
+
}
|
|
162
|
+
return keys;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=gallery.js.map
|
|
@@ -89,10 +89,15 @@ export declare function isAllowedEmbedHtmlAttributeName({ name }: {
|
|
|
89
89
|
* therefore the ONLY path to acceptance for a name; nothing here tries to sanitize an otherwise-
|
|
90
90
|
* disallowed name or value into something safe.
|
|
91
91
|
*
|
|
92
|
+
* @param options.styleContext Use `stylesheet` only for a style element's CSS text; allows
|
|
93
|
+
* non-ASCII scalar hex escapes for glyphs while preserving the inline escape restriction.
|
|
94
|
+
*
|
|
92
95
|
* @complexity Time O(n) in `text`'s length (one regex pass over it), space O(k) for k accepted
|
|
93
96
|
* attributes plus O(e) for e rejected tokens.
|
|
94
97
|
*/
|
|
95
98
|
export declare function parseEmbedHtmlAttributes({ text }: {
|
|
96
99
|
text: string;
|
|
97
|
-
},
|
|
100
|
+
}, options?: {
|
|
101
|
+
styleContext?: "stylesheet";
|
|
102
|
+
}): ParsedEmbedHtmlAttributes;
|
|
98
103
|
//# sourceMappingURL=html-attributes.d.ts.map
|
|
@@ -116,16 +116,28 @@ function isUnsafeUrlValue(value) {
|
|
|
116
116
|
* Whether a `style` value carries a known CSS injection or CSS-escape-bypass vector:
|
|
117
117
|
* `expression(...)` (legacy IE script-in-CSS), a `javascript:`/`vbscript:` value inside a CSS
|
|
118
118
|
* `url(...)`, `-moz-binding`/`behavior:` (XBL/HTC script bindings), `@import` (pulls in a second,
|
|
119
|
-
* unvetted stylesheet), or ANY backslash — a CSS escape sequence (`\65`
|
|
120
|
-
* the above past a plain substring check, so a backslash anywhere in
|
|
119
|
+
* unvetted stylesheet), or ANY backslash in an inline attribute — a CSS escape sequence (`\65`
|
|
120
|
+
* is `e`) can respell any of the above past a plain substring check, so a backslash anywhere in
|
|
121
|
+
* an inline value is rejected outright
|
|
121
122
|
* rather than trying to first decode every possible CSS escape. Plain `url(...)` values (background
|
|
122
123
|
* images) are otherwise allowed — authors here are the owner/admins, not anonymous input.
|
|
124
|
+
* Stylesheets additionally allow non-ASCII scalar hex escapes (e.g. `\2713` for a checkmark).
|
|
125
|
+
* They cannot respell the ASCII injection vocabulary. Only the validation projection changes;
|
|
126
|
+
* the accepted value remains byte-for-byte authored CSS. ASCII/invalid/other escapes still fail.
|
|
123
127
|
*
|
|
124
|
-
* @complexity O(n) in the value's length
|
|
128
|
+
* @complexity O(n) time/space in the value's length; escape matches consume at most six digits.
|
|
125
129
|
*/
|
|
126
130
|
const UNSAFE_STYLE_PATTERN = /expression\(|javascript:|vbscript:|-moz-binding|behavior:|@import|\\/i;
|
|
127
|
-
function isUnsafeStyleValue(value) {
|
|
128
|
-
|
|
131
|
+
function isUnsafeStyleValue(value, { styleContext } = {}) {
|
|
132
|
+
const inspected = styleContext === "stylesheet"
|
|
133
|
+
? value.replace(/\\([0-9a-f]{1,6})(?:\r\n|[ \t\n\r\f])?/gi, (escape, hex) => {
|
|
134
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
135
|
+
if (codePoint < 0xa0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff))
|
|
136
|
+
return escape;
|
|
137
|
+
return String.fromCodePoint(codePoint);
|
|
138
|
+
})
|
|
139
|
+
: value;
|
|
140
|
+
return UNSAFE_STYLE_PATTERN.test(inspected);
|
|
129
141
|
}
|
|
130
142
|
/**
|
|
131
143
|
* Classifies one already-tokenized `name`/`value` pair into a rejection reason, or `null` when it is
|
|
@@ -144,14 +156,14 @@ function isUnsafeStyleValue(value) {
|
|
|
144
156
|
* @complexity O(1) — four fixed checks against one already-extracted token; each check's own O(n) in
|
|
145
157
|
* the token's length is linear, not nested.
|
|
146
158
|
*/
|
|
147
|
-
function classifyEmbedHtmlAttributeToken(name, value) {
|
|
159
|
+
function classifyEmbedHtmlAttributeToken(name, value, options = {}) {
|
|
148
160
|
if (name.startsWith("on"))
|
|
149
161
|
return { reason: "event-handler", attribute: name };
|
|
150
162
|
if (isUnsafeUrlValue(value))
|
|
151
163
|
return { reason: "unsafe-url", attribute: name };
|
|
152
164
|
if (!isAllowedEmbedHtmlAttributeName({ name: name }))
|
|
153
165
|
return { reason: "disallowed-name", attribute: name };
|
|
154
|
-
if (name === "style" && isUnsafeStyleValue(value))
|
|
166
|
+
if (name === "style" && isUnsafeStyleValue(value, options))
|
|
155
167
|
return { reason: "unsafe-style", attribute: name };
|
|
156
168
|
return null;
|
|
157
169
|
}
|
|
@@ -172,10 +184,13 @@ const HTML_ATTRIBUTE_TOKEN = /([^\s="']+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s"']+)))
|
|
|
172
184
|
* therefore the ONLY path to acceptance for a name; nothing here tries to sanitize an otherwise-
|
|
173
185
|
* disallowed name or value into something safe.
|
|
174
186
|
*
|
|
187
|
+
* @param options.styleContext Use `stylesheet` only for a style element's CSS text; allows
|
|
188
|
+
* non-ASCII scalar hex escapes for glyphs while preserving the inline escape restriction.
|
|
189
|
+
*
|
|
175
190
|
* @complexity Time O(n) in `text`'s length (one regex pass over it), space O(k) for k accepted
|
|
176
191
|
* attributes plus O(e) for e rejected tokens.
|
|
177
192
|
*/
|
|
178
|
-
export function parseEmbedHtmlAttributes({ text },
|
|
193
|
+
export function parseEmbedHtmlAttributes({ text }, options = {}) {
|
|
179
194
|
const trimmed = text.trim();
|
|
180
195
|
if (trimmed === "")
|
|
181
196
|
return { attributes: {}, errors: [], error: null };
|
|
@@ -198,7 +213,7 @@ export function parseEmbedHtmlAttributes({ text }, _optional = {}) {
|
|
|
198
213
|
consumed = match.index + match[0].length;
|
|
199
214
|
const name = (match[1] ?? "").toLowerCase();
|
|
200
215
|
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
201
|
-
const rejection = classifyEmbedHtmlAttributeToken(name, value);
|
|
216
|
+
const rejection = classifyEmbedHtmlAttributeToken(name, value, options);
|
|
202
217
|
if (rejection) {
|
|
203
218
|
errors.push(rejection);
|
|
204
219
|
continue;
|
|
@@ -99,6 +99,10 @@ export interface ScanEmbedMarkersResult {
|
|
|
99
99
|
*/
|
|
100
100
|
readonly rejected: readonly EmbedMarkerRejection[];
|
|
101
101
|
}
|
|
102
|
+
/** Shared decoding seam for authored media references; parsing and indexing use one entity policy. */
|
|
103
|
+
export declare function decodeMarkerHtmlAttribute({ value }: {
|
|
104
|
+
value: string;
|
|
105
|
+
}, _optional?: Record<string, never>): string;
|
|
102
106
|
/**
|
|
103
107
|
* Produce a same-length copy of `html` with every HTML comment, and the raw-text content of every
|
|
104
108
|
* `<script>`/`<style>` element, replaced by space filler. {@link scanEmbedMarkers} runs
|
|
@@ -89,6 +89,10 @@ function decodeAttributeValue(value) {
|
|
|
89
89
|
return codePoint > 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : whole;
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
|
+
/** Shared decoding seam for authored media references; parsing and indexing use one entity policy. */
|
|
93
|
+
export function decodeMarkerHtmlAttribute({ value }, _optional = {}) {
|
|
94
|
+
return decodeAttributeValue(value);
|
|
95
|
+
}
|
|
92
96
|
/** The inverse of {@link decodeAttributeValue} for a value written back double-quoted. */
|
|
93
97
|
function encodeDoubleQuotedValue(value) {
|
|
94
98
|
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
@@ -512,6 +516,8 @@ export const EMBED_MARKER_TARGET_KEYS = {
|
|
|
512
516
|
form: ["id", "slug"],
|
|
513
517
|
taxonomy: ["id"],
|
|
514
518
|
media: ["id", "slug"],
|
|
519
|
+
// Inline ordered images, not a persisted gallery entity; each mediaId is its own reference.
|
|
520
|
+
gallery: [],
|
|
515
521
|
post: ["id", "slug"],
|
|
516
522
|
content: ["id", "slug"],
|
|
517
523
|
[COLLECTION_MARKER_TYPE]: ["id", "typeKey"],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Mirrors the public renderer's `MAX_MEDIA_REF_ID_LENGTH`/`PLAUSIBLE_MEDIA_REF_ID_PATTERN`
|
|
2
|
+
* shape check for an assetId/transformName that will become a `/m/` URL path segment. Widgets
|
|
3
|
+
* must not depend on the host renderer above it in the layering: this check guards repository
|
|
4
|
+
* lookup keys, and the renderer re-checks immediately before URL templating as defense in depth.
|
|
5
|
+
* Shared by media and gallery config so their media-reference contracts cannot drift.
|
|
6
|
+
* @complexity O(n) in the reference length, with a 200-character bound; no I/O.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isPlausibleMediaRefId({ value }: {
|
|
9
|
+
value: unknown;
|
|
10
|
+
}, _optional?: Record<string, never>): boolean;
|
|
11
|
+
//# sourceMappingURL=media-reference.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Mirrors the public renderer's `MAX_MEDIA_REF_ID_LENGTH`/`PLAUSIBLE_MEDIA_REF_ID_PATTERN`
|
|
2
|
+
* shape check for an assetId/transformName that will become a `/m/` URL path segment. Widgets
|
|
3
|
+
* must not depend on the host renderer above it in the layering: this check guards repository
|
|
4
|
+
* lookup keys, and the renderer re-checks immediately before URL templating as defense in depth.
|
|
5
|
+
* Shared by media and gallery config so their media-reference contracts cannot drift.
|
|
6
|
+
* @complexity O(n) in the reference length, with a 200-character bound; no I/O.
|
|
7
|
+
*/
|
|
8
|
+
export function isPlausibleMediaRefId({ value }, _optional = {}) {
|
|
9
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 200 && /^[^\s/]+$/.test(value);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=media-reference.js.map
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* ~990 modules and seconds of cold start per render (see `__tests__/worker-import-graph.test.ts`
|
|
11
11
|
* under `server/inbound/public-http/http/site/`).
|
|
12
12
|
*/
|
|
13
|
-
export declare const PAGE_EMBED_TYPES: readonly ["widget", "form", "taxonomy", "media", "post", "content"];
|
|
13
|
+
export declare const PAGE_EMBED_TYPES: readonly ["widget", "form", "taxonomy", "media", "gallery", "post", "content"];
|
|
14
14
|
export type PageEmbedType = (typeof PAGE_EMBED_TYPES)[number];
|
|
15
15
|
/**
|
|
16
16
|
* Does the page-embed stage OWN this marker type — i.e. is a REQ-28 placeholder the honest answer
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* ~990 modules and seconds of cold start per render (see `__tests__/worker-import-graph.test.ts`
|
|
11
11
|
* under `server/inbound/public-http/http/site/`).
|
|
12
12
|
*/
|
|
13
|
-
export const PAGE_EMBED_TYPES = ["widget", "form", "taxonomy", "media", "post", "content"];
|
|
13
|
+
export const PAGE_EMBED_TYPES = ["widget", "form", "taxonomy", "media", "gallery", "post", "content"];
|
|
14
14
|
const PAGE_EMBED_TYPE_SET = new Set(PAGE_EMBED_TYPES);
|
|
15
15
|
/**
|
|
16
16
|
* Does the page-embed stage OWN this marker type — i.e. is a REQ-28 placeholder the honest answer
|