@jini-ai/cms 0.5.5 → 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/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 +51 -6
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Core type definitions for the Tovu `routing` Tier-2 library (ADR-039 v0).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Declares the `RouteTarget` vocabulary, the workspace-scoped resolve context,
|
|
6
|
+
* the inverse-resolver result shape, the forward resolution pipeline's phase
|
|
7
|
+
* vocabulary, and the `SlugChangeCapture` slot's payload shape. This is the one
|
|
8
|
+
* place `entryRef | termRef | url | route` is defined — promoted from
|
|
9
|
+
* Menus-local (ADR-029 §3) to routing-owned per ADR-039 §3, so Menus/SEO/
|
|
10
|
+
* Redirects share one vocabulary instead of inventing three.
|
|
11
|
+
*
|
|
12
|
+
* How it relates to the project:
|
|
13
|
+
* - `core/ports.ts` supplies the shared primitives (`UUID`, `ISODateTime`, …).
|
|
14
|
+
* - `routing/ports.ts` declares the dependency shape `urlFor`/`isActive` need
|
|
15
|
+
* (the existing `PostRepoPort`, reused rather than duplicated).
|
|
16
|
+
* - `routing/routing.ts` implements the resolve functions against these types.
|
|
17
|
+
*
|
|
18
|
+
* Architectural role:
|
|
19
|
+
* INTERFACES + TYPES ONLY (no feature logic). ADR-039 v0 is deliberately the
|
|
20
|
+
* minimal shape three other sections depend on; full permalink-structure
|
|
21
|
+
* design is explicitly deferred (ADR-039 "Open").
|
|
22
|
+
*/
|
|
23
|
+
import type { JsonObject, UUID } from "@jini-ai/core/primitives";
|
|
24
|
+
/** Discriminant values for {@link RouteTarget}. */
|
|
25
|
+
export type RouteTargetKind = "entryRef" | "termRef" | "url" | "route";
|
|
26
|
+
/**
|
|
27
|
+
* A link to a content entry by id. Only `post` has a real resolver in this
|
|
28
|
+
* repo today (ADR-039 v0); `contentType` is accepted for forward-compat with
|
|
29
|
+
* future content types but is not required to resolve a post reference.
|
|
30
|
+
*/
|
|
31
|
+
export interface EntryRefTarget {
|
|
32
|
+
readonly kind: "entryRef";
|
|
33
|
+
readonly entryId: UUID;
|
|
34
|
+
readonly contentType?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A link to a taxonomy term (category/tag archive).
|
|
38
|
+
*
|
|
39
|
+
* NOT resolvable today: taxonomy isn't implemented in this repo, and ADR-029's
|
|
40
|
+
* Round-3 audit fold promoted "term-link schema" from an Open item to an
|
|
41
|
+
* explicit Wave-1 acceptance blocker — ADR-022 §5's `entry_refs` covers
|
|
42
|
+
* entry-to-entry references only, with no term-target schema yet (content-lib
|
|
43
|
+
* sign-off owed). `urlFor`/`isActive` resolve every `termRef` target to `null`
|
|
44
|
+
* until that schema lands; see the stub in `routing.ts`.
|
|
45
|
+
*/
|
|
46
|
+
export interface TermRefTarget {
|
|
47
|
+
readonly kind: "termRef";
|
|
48
|
+
readonly termId: UUID;
|
|
49
|
+
readonly taxonomy: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* An external or site-relative URL, passed through as given (scheme-validated:
|
|
53
|
+
* `javascript:`/`data:` are rejected and resolve to `null`, mirroring the
|
|
54
|
+
* "unavailable → null" contract rather than throwing).
|
|
55
|
+
*/
|
|
56
|
+
export interface UrlTarget {
|
|
57
|
+
readonly kind: "url";
|
|
58
|
+
readonly href: string;
|
|
59
|
+
}
|
|
60
|
+
/** A named core route (e.g. `home`, `search`), resolved via the route registry. */
|
|
61
|
+
export interface RouteRefTarget {
|
|
62
|
+
readonly kind: "route";
|
|
63
|
+
readonly route: string;
|
|
64
|
+
/** Optional static params; unused by the v0 registry, reserved for v0.1 param interpolation. */
|
|
65
|
+
readonly params?: JsonObject;
|
|
66
|
+
}
|
|
67
|
+
/** v0 discriminated union of route targets (ADR-039 §3). */
|
|
68
|
+
export type RouteTarget = EntryRefTarget | TermRefTarget | UrlTarget | RouteRefTarget;
|
|
69
|
+
/**
|
|
70
|
+
* Workspace-scoped context threaded through every resolve call. `siteId`/
|
|
71
|
+
* `locale`/`originKey` are reserved for multi-site/i18n v0.1 work (ADR-039
|
|
72
|
+
* "Open"); only `workspaceId` is load-bearing today.
|
|
73
|
+
*
|
|
74
|
+
* `originOverride` is a stand-in for `core/origin` (ADR-040), which is being
|
|
75
|
+
* built in parallel and does not exist yet. Once it lands, `canonicalUrl`
|
|
76
|
+
* composition should read the verified origin from
|
|
77
|
+
* `OriginRegistryPort.canonicalOrigin(ctx)` instead of this field.
|
|
78
|
+
*/
|
|
79
|
+
export interface RouteResolveContext {
|
|
80
|
+
readonly workspaceId: UUID;
|
|
81
|
+
readonly siteId?: string;
|
|
82
|
+
readonly locale?: string;
|
|
83
|
+
readonly originKey?: string;
|
|
84
|
+
/**
|
|
85
|
+
* TODO(ADR-040): compose canonicalUrl from OriginRegistryPort.canonicalOrigin(ctx)
|
|
86
|
+
* once src/origin exists. Until then, a caller that knows the verified origin
|
|
87
|
+
* may pass it here; absent, canonicalUrl falls back to the relative path.
|
|
88
|
+
*/
|
|
89
|
+
readonly originOverride?: string;
|
|
90
|
+
}
|
|
91
|
+
/** Result of a successful inverse resolve — the shape Menus/SEO consume. */
|
|
92
|
+
export interface RouteUrl {
|
|
93
|
+
readonly path: string;
|
|
94
|
+
readonly canonicalUrl: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The two registerable phases of the forward pipeline
|
|
98
|
+
* (`normalize → pre_content → content-resolve → post_content → 404`).
|
|
99
|
+
* `content-resolve` is fixed core content lookup and is not a registerable
|
|
100
|
+
* phase of this library; it is out of scope for ADR-039 v0's registration
|
|
101
|
+
* mechanism.
|
|
102
|
+
*/
|
|
103
|
+
export type RouteResolvePhaseName = "pre_content" | "post_content";
|
|
104
|
+
/** Terminal decision a phase resolver may return; anything else falls through. */
|
|
105
|
+
export type RouteResolvePhaseOutcome = {
|
|
106
|
+
readonly kind: "redirect";
|
|
107
|
+
readonly location: string;
|
|
108
|
+
readonly statusCode: number;
|
|
109
|
+
} | {
|
|
110
|
+
readonly kind: "resolved";
|
|
111
|
+
readonly path: string;
|
|
112
|
+
} | {
|
|
113
|
+
readonly kind: "not_found";
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* A single phase resolver. Returns `null` to fall through to the next
|
|
117
|
+
* registered resolver in the same phase (and eventually the next phase);
|
|
118
|
+
* returns an outcome to short-circuit the pipeline (ADR-039 §1 invariant:
|
|
119
|
+
* `pre_content` may only redirect/gone, never turn a resolvable live path
|
|
120
|
+
* into a 404 — Round-2 amendment 5).
|
|
121
|
+
*/
|
|
122
|
+
export type RouteResolvePhaseHandler = (path: string, ctx: RouteResolveContext) => Promise<RouteResolvePhaseOutcome | null>;
|
|
123
|
+
/** Final outcome of {@link resolve} after all phases have run. */
|
|
124
|
+
export type RouteResolveResult = {
|
|
125
|
+
readonly matched: false;
|
|
126
|
+
} | {
|
|
127
|
+
readonly matched: true;
|
|
128
|
+
readonly phase: RouteResolvePhaseName;
|
|
129
|
+
readonly outcome: RouteResolvePhaseOutcome;
|
|
130
|
+
};
|
|
131
|
+
/** Payload handed to the bound {@link SlugChangeCapture} inside the rename transaction. */
|
|
132
|
+
export interface SlugChangeCaptureInput {
|
|
133
|
+
readonly workspaceId: UUID;
|
|
134
|
+
readonly entryId: UUID;
|
|
135
|
+
readonly oldPath: string;
|
|
136
|
+
readonly newPath: string;
|
|
137
|
+
readonly actor: UUID;
|
|
138
|
+
readonly changeSetId?: UUID;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The single named slot content's write chokepoint calls inside the rename
|
|
142
|
+
* transaction (ADR-039 §4). Declared here by routing (content knows nothing of
|
|
143
|
+
* redirects); implemented by the redirects library; wired by the composition
|
|
144
|
+
* root. **No slot implementation may be plugin-supplied** (Round-2 amendment 1
|
|
145
|
+
* — the slot is core-resident by definition).
|
|
146
|
+
*
|
|
147
|
+
* Constraints on any bound implementation (Round-2 amendment 2): idempotent by
|
|
148
|
+
* `changeSetId`, no external I/O, no in-tx publish (an outbox row may be
|
|
149
|
+
* written in-tx per the transactional-outbox pattern, but dispatch happens
|
|
150
|
+
* post-commit), and a strict lock order of content row before redirect rows.
|
|
151
|
+
*
|
|
152
|
+
* Fail-closed nuance (Round-2 amendment 3, not enforced by this library):
|
|
153
|
+
* absent binding is a no-op only before Redirects is installed, or while
|
|
154
|
+
* slug-preservation is disabled for the workspace. Once link-preservation is
|
|
155
|
+
* enabled, a slug-changing rename with no bound capture MUST fail
|
|
156
|
+
* `LINK_PRESERVATION_UNAVAILABLE` rather than silently skip the redirect. That
|
|
157
|
+
* enforcement belongs to the content write chokepoint (which knows whether
|
|
158
|
+
* preservation is enabled) and to Redirects (which doesn't exist yet in this
|
|
159
|
+
* repo) — this library only exposes the registration slot.
|
|
160
|
+
*/
|
|
161
|
+
export interface SlugChangeCapture {
|
|
162
|
+
onSlugChange(input: SlugChangeCaptureInput): Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/seo/media.d.ts
CHANGED
|
@@ -29,6 +29,29 @@ export declare function resolveSeoImageRef(required: {
|
|
|
29
29
|
deps: ResolveSeoImageRefDeps;
|
|
30
30
|
input: ResolveSeoImageRefInput;
|
|
31
31
|
}, _optional?: Record<string, never>): Promise<string | undefined>;
|
|
32
|
+
/** Metadata belongs to the selected transform, never the asset's editable display-size fields. */
|
|
33
|
+
export interface SeoImageDetails {
|
|
34
|
+
url: string;
|
|
35
|
+
/** Original source choice for admin diagnostics; a resized rendition can conceal a tiny source. */
|
|
36
|
+
ref?: string | undefined;
|
|
37
|
+
width?: number | undefined;
|
|
38
|
+
height?: number | undefined;
|
|
39
|
+
type?: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Resolve through the existing media owners, optionally selecting a host's share transform.
|
|
42
|
+
* Absolute external URLs remain unchanged with unknown metadata; no remote fetch occurs.
|
|
43
|
+
* The latest definition keeps the anonymous lazy-generation guarantee documented above.
|
|
44
|
+
* @param required Media ports, workspace and persisted reference.
|
|
45
|
+
* @param optional Host-selected transform; omitted or unregistered retains the reference's named transform.
|
|
46
|
+
* @returns URL and known output metadata, or undefined on a lookup miss.
|
|
47
|
+
* @complexity O(v) in registered transform versions; up to three bounded repository reads.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveSeoImageDetails(required: {
|
|
50
|
+
deps: ResolveSeoImageRefDeps;
|
|
51
|
+
input: ResolveSeoImageRefInput;
|
|
52
|
+
}, optional?: {
|
|
53
|
+
transformName?: string | undefined;
|
|
54
|
+
}): Promise<SeoImageDetails | undefined>;
|
|
32
55
|
/**
|
|
33
56
|
* The write-path refusal for an `ogImage`/`twitterImage`/`defaultOgImage` value (2026-10-05): the
|
|
34
57
|
* message to refuse it with, or `null` to accept it. Both SEO write chokepoints call it
|
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
|