@getquick/content 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +179 -2
  2. package/consent-migrations/0001_consent_records.sql +34 -0
  3. package/dist/bearer-DMOCoSci.js +16 -0
  4. package/dist/client-config-B3LNSOxl.js +4 -0
  5. package/dist/configuration-C_JGygQI.d.ts +259 -0
  6. package/dist/consent-astro.d.ts +36 -0
  7. package/dist/consent-astro.js +94 -0
  8. package/dist/consent-client.d.ts +37 -0
  9. package/dist/consent-client.js +321 -0
  10. package/dist/consent-migrations.d.ts +8 -0
  11. package/dist/consent-migrations.js +10 -0
  12. package/dist/consent-server.d.ts +77 -0
  13. package/dist/consent-server.js +292 -0
  14. package/dist/consent-testing.d.ts +96 -0
  15. package/dist/consent-testing.js +188 -0
  16. package/dist/consent.d.ts +114 -0
  17. package/dist/consent.js +5 -0
  18. package/dist/embeds-p9SyGNZI.js +344 -0
  19. package/dist/formatting-wOk2jz6J.js +276 -0
  20. package/dist/index.d.ts +75 -0
  21. package/dist/index.js +39 -0
  22. package/dist/legal-C5Xiq59W.d.ts +48 -0
  23. package/dist/migrations.d.ts +4 -0
  24. package/dist/migrations.js +6 -0
  25. package/dist/paths-Cmzzof3v.js +5 -0
  26. package/dist/records-CACA2O0Q.d.ts +56 -0
  27. package/dist/records-Dumrz759.js +74 -0
  28. package/dist/server.d.ts +2 -0
  29. package/dist/server.js +6465 -0
  30. package/dist/state-BYWc5TUB.js +559 -0
  31. package/dist/state-Dzd52F3c.d.ts +77 -0
  32. package/dist/version-DyyNhsuU.js +6 -0
  33. package/dist/wordpress-CUFb6YZZ.d.ts +384 -0
  34. package/migrations/0001_publications.sql +31 -0
  35. package/migrations/0002_entries.sql +14 -0
  36. package/migrations/0003_publication_events.sql +36 -0
  37. package/migrations/0004_withdrawals.sql +30 -0
  38. package/migrations/0005_reconciliation.sql +45 -0
  39. package/package.json +56 -3
@@ -0,0 +1,276 @@
1
+ //#region frontend/src/wp-container-layout.ts
2
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
3
+ /** WPGraphQL renders nested blocks independently. Restore the parent's resolved
4
+ * variables on direct container children without replacing their own layout. */
5
+ function containerChildLayoutHtml(html, parentHtml) {
6
+ const parentTag = parentHtml.match(/^\s*<div\b[^>]*>/i)?.[0] ?? "";
7
+ if (!/\bgqd-container-layout\b/.test(parentTag)) return html;
8
+ const declarations = [...(parentTag.match(/\sstyle="([^"]*)"/)?.[1] ?? "").matchAll(/--gqd-layout-([a-z-]+):([^;"']+)/g)].filter(([, , value]) => value === "auto" || layoutValue(value)).map(([, name, value]) => `--gqd-parent-layout-${name}:${value}`).join(";");
9
+ if (!declarations) return html;
10
+ return html.replace(/^(\s*<div\b)([^>]*)(>)/i, (_, tag, props, end) => {
11
+ const style = /\sstyle="([^"]*)"/;
12
+ props = style.test(props) ? props.replace(style, (_match, old) => ` style="${old};${declarations}"`) : `${props} style="${declarations}"`;
13
+ return `${tag}${props}${end}`;
14
+ });
15
+ }
16
+ function layoutValue(value) {
17
+ if (typeof value !== "string" && typeof value !== "number") return "";
18
+ const text = String(value);
19
+ const preset = text.match(/^var:preset\|spacing\|([a-z0-9-]+)$/);
20
+ if (preset) return `var(--wp--preset--spacing--${preset[1]})`;
21
+ return /^(?:none|0|[0-9.]+(?:px|rem|em|%|vw|vh|ch)|(?:var|calc|min|max|clamp)\([a-zA-Z0-9_.,%()+*/ -]+\))$/.test(text) ? text : "";
22
+ }
23
+ /** Rendered CMS shells already carry resolved theme defaults. Saved shells need
24
+ * the same opt-in adapter; absent defaults deliberately fall back to no width.
25
+ */
26
+ function containerLayoutHtml(html, attributes) {
27
+ if (/\bgqd-container-layout\b/.test(html.match(/^\s*<[^>]+>/)?.[0] ?? "")) return html;
28
+ const layout = record(attributes.layout);
29
+ const spacing = record(record(attributes.style).spacing);
30
+ const gap = spacing.blockGap;
31
+ if (!Object.keys(layout).length && (gap === void 0 || gap === null || gap === "")) return html;
32
+ const type = layout.inherit || layout.contentSize ? "constrained" : layout.type ?? "default";
33
+ if (type !== "default" && type !== "constrained") return html;
34
+ const content = layoutValue(layout.contentSize);
35
+ const wide = layoutValue(layout.wideSize);
36
+ const variables = [
37
+ `--gqd-layout-content:${content || wide || "var(--wp--style--global--content-size, none)"}`,
38
+ `--gqd-layout-wide:${wide || content || "var(--wp--style--global--wide-size, none)"}`,
39
+ `--gqd-layout-gap:${layoutValue(typeof gap === "object" ? record(gap).top : gap) || "var(--wp--style--block-gap, 0)"}`
40
+ ];
41
+ let classes = "gqd-container-layout";
42
+ if (type === "constrained") {
43
+ classes += " gqd-container-layout--constrained";
44
+ variables.push(`--gqd-layout-left:${layout.justifyContent === "left" ? "0" : "auto"}`, `--gqd-layout-right:${layout.justifyContent === "right" ? "0" : "auto"}`);
45
+ for (const side of ["left", "right"]) variables.push(`--gqd-layout-padding-${side}:${layoutValue(typeof spacing.padding === "object" ? record(spacing.padding)[side] : spacing.padding) || "0px"}`);
46
+ }
47
+ return (html || "<div class=\"wp-block-getquick-design-container\"></div>").replace(/^(\s*<div\b)([^>]*)(>)/i, (_, tag, props, end) => {
48
+ const add = (name, value, separator) => {
49
+ const pattern = new RegExp(`(\\s)${name}="([^"]*)"`);
50
+ props = pattern.test(props) ? props.replace(pattern, (_match, space, old) => `${space}${name}="${old}${separator}${value}"`) : `${props} ${name}="${value}"`;
51
+ };
52
+ add("class", classes, " ");
53
+ add("style", variables.join(";"), ";");
54
+ return `${tag}${props}${end}`;
55
+ });
56
+ }
57
+ //#endregion
58
+ //#region frontend/src/wp-block-renderer.ts
59
+ const VIDEO_HERO_BLOCK = "getquick-design/video-hero";
60
+ const YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
61
+ const BLOCKS_WITH_RENDERED_CHILDREN = /* @__PURE__ */ new Set([
62
+ "core/cover",
63
+ "core/media-text",
64
+ "core/navigation",
65
+ "core/navigation-submenu"
66
+ ]);
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
+ }
70
+ function normalizeBlock(value) {
71
+ if (!isRecord(value) || typeof value.name !== "string") return null;
72
+ const attributes = isRecord(value.attributes) ? value.attributes : void 0;
73
+ const innerBlocks = Array.isArray(value.innerBlocks) ? value.innerBlocks.map(normalizeBlock).filter((block) => block !== null) : [];
74
+ return {
75
+ name: value.name,
76
+ attributes,
77
+ htmlContent: typeof value.htmlContent === "string" ? value.htmlContent : "",
78
+ dynamicContent: typeof value.dynamicContent === "string" ? value.dynamicContent : void 0,
79
+ innerBlocks
80
+ };
81
+ }
82
+ function parseWordPressBlocks(value) {
83
+ let parsed = value;
84
+ for (let attempt = 0; attempt < 2 && typeof parsed === "string"; attempt += 1) try {
85
+ parsed = JSON.parse(parsed);
86
+ } catch {
87
+ return [];
88
+ }
89
+ if (!Array.isArray(parsed)) return [];
90
+ return parsed.map(normalizeBlock).filter((block) => block !== null);
91
+ }
92
+ function hasVideoHero(blocks) {
93
+ return blocks.some((block) => block.name === "getquick-design/video-hero" || hasVideoHero(block.innerBlocks ?? []));
94
+ }
95
+ function escapeAttribute(value) {
96
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
97
+ }
98
+ function safeMediaUrl(value) {
99
+ if (typeof value !== "string") return "";
100
+ const url = value.trim();
101
+ if (!url || url.startsWith("//")) return "";
102
+ if (url.startsWith("/")) return url;
103
+ try {
104
+ const parsed = new URL(url);
105
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? url : "";
106
+ } catch {
107
+ return "";
108
+ }
109
+ }
110
+ function youtubeVideoId(value) {
111
+ try {
112
+ const url = new URL(value);
113
+ if (url.protocol !== "https:" && url.protocol !== "http:") return "";
114
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
115
+ let id = "";
116
+ if (host === "youtu.be") id = url.pathname.split("/").filter(Boolean)[0] ?? "";
117
+ else if ([
118
+ "youtube.com",
119
+ "m.youtube.com",
120
+ "youtube-nocookie.com"
121
+ ].includes(host)) {
122
+ id = url.searchParams.get("v") ?? "";
123
+ if (!id) id = url.pathname.match(/^\/(?:embed|shorts|live)\/([^/]+)/)?.[1] ?? "";
124
+ }
125
+ return YOUTUBE_ID_PATTERN.test(id) ? id : "";
126
+ } catch {
127
+ return "";
128
+ }
129
+ }
130
+ function videoMarkup(attributes) {
131
+ const posterUrl = safeMediaUrl(attributes.posterUrl);
132
+ const poster = posterUrl ? `<img class="wp-block-getquick-design-video-hero__poster" src="${escapeAttribute(posterUrl)}" alt="" decoding="async" />` : "";
133
+ if (attributes.videoType === "youtube") {
134
+ const videoId = typeof attributes.videoUrl === "string" ? youtubeVideoId(attributes.videoUrl) : "";
135
+ if (!videoId) return poster;
136
+ return `${poster}<iframe class="wp-block-getquick-design-video-hero__video wp-block-getquick-design-video-hero__video--youtube" src="https://www.youtube-nocookie.com/embed/${videoId}?${new URLSearchParams({
137
+ autoplay: "1",
138
+ mute: "1",
139
+ controls: "0",
140
+ loop: "1",
141
+ playlist: videoId,
142
+ playsinline: "1",
143
+ rel: "0"
144
+ }).toString()}" title="Decorative background video" allow="autoplay; encrypted-media" referrerpolicy="strict-origin-when-cross-origin" tabindex="-1" aria-hidden="true"></iframe>`;
145
+ }
146
+ const videoUrl = safeMediaUrl(attributes.videoUrl);
147
+ if (!videoUrl) return poster;
148
+ const posterAttribute = posterUrl ? ` poster="${escapeAttribute(posterUrl)}"` : "";
149
+ return `<video class="wp-block-getquick-design-video-hero__video" src="${escapeAttribute(videoUrl)}"${posterAttribute} autoplay muted loop playsinline preload="metadata" aria-hidden="true"></video>`;
150
+ }
151
+ function rootSectionShell(html) {
152
+ return html.match(/^\s*(<section\b[^>]*>)/i)?.[1] ?? "<section class=\"wp-block-getquick-design-video-hero\">";
153
+ }
154
+ function renderVideoHero(block, children) {
155
+ return `${rootSectionShell(block.htmlContent ?? "")}<div class="wp-block-getquick-design-video-hero__media" aria-hidden="true">${videoMarkup(block.attributes ?? {})}</div><div class="wp-block-getquick-design-video-hero__content">${children}</div></section>`;
156
+ }
157
+ function appendChildren(html, children) {
158
+ if (!children) return html;
159
+ const closingTag = html.lastIndexOf("</");
160
+ if (closingTag < 0) return `${html}${children}`;
161
+ return `${html.slice(0, closingTag)}${children}${html.slice(closingTag)}`;
162
+ }
163
+ function renderBlock(block, parentContainerHtml = "") {
164
+ const isContainer = block.name === "getquick-design/container";
165
+ let html = isContainer ? containerLayoutHtml(block.htmlContent ?? "", block.attributes ?? {}) : block.htmlContent ?? "";
166
+ if (isContainer && parentContainerHtml) html = containerChildLayoutHtml(html, parentContainerHtml);
167
+ const children = (block.innerBlocks ?? []).map((child) => renderBlock(child, isContainer ? html : "")).join("");
168
+ if (block.name === "getquick-design/video-hero") return renderVideoHero(block, children);
169
+ if (!children || BLOCKS_WITH_RENDERED_CHILDREN.has(block.name)) return html;
170
+ return appendChildren(html, children);
171
+ }
172
+ function renderWordPressBlocks(blocks) {
173
+ return blocks.map((block) => renderBlock(block)).join("");
174
+ }
175
+ //#endregion
176
+ //#region frontend/src/site-language.ts
177
+ /**
178
+ * The `<html lang>` for a WordPress locale: its language and region as a
179
+ * BCP 47 tag (pt_PT_ao90 → pt-PT). WordPress's variants (ao90, formal) aren't
180
+ * BCP 47 subtags, so they are left out; no locale means English.
181
+ */
182
+ function htmlLang(locale) {
183
+ if (!locale) return "en";
184
+ return locale.split("_").slice(0, 2).join("-");
185
+ }
186
+ /**
187
+ * configuration's languages: wordpress.locale, the default, served at /, then
188
+ * each of wordpress.languages under its slug. A monolingual Site has one.
189
+ */
190
+ function siteLanguages(wordpress) {
191
+ const locale = wordpress?.locale ?? "";
192
+ const language = (locale, slug, isDefault) => ({
193
+ locale,
194
+ slug,
195
+ code: slug.toUpperCase().replaceAll("-", "_"),
196
+ lang: htmlLang(locale),
197
+ isDefault,
198
+ home: isDefault ? "/" : `/${slug}/`
199
+ });
200
+ return [language(locale, locale.split("_")[0] || "en", true), ...(wordpress?.languages ?? []).map(({ locale, slug }) => language(locale, slug, false))];
201
+ }
202
+ /**
203
+ * A route's language: the one whose slug is its first segment, else the
204
+ * default language, which has no directory.
205
+ */
206
+ function routeLanguage(route, among) {
207
+ const segment = route.split("/")[1];
208
+ return among.find((language) => !language.isDefault && language.slug === segment) ?? among[0];
209
+ }
210
+ /**
211
+ * How logs and reports name something a language has: "the front page" for
212
+ * the default language's, "the en front page" for another's.
213
+ */
214
+ function languageSubject(language, what) {
215
+ return language.isDefault ? `the ${what}` : `the ${language.slug} ${what}`;
216
+ }
217
+ /** Whether a route is a language's home (/, /en/): served as a home, never as an entry. */
218
+ function isLanguageHome(route, among) {
219
+ return among.some((language) => language.home === route);
220
+ }
221
+ //#endregion
222
+ //#region frontend/src/formatting.ts
223
+ function responseStatus(delivery) {
224
+ if (delivery.kind === "found") return 200;
225
+ return delivery.kind === "missing" ? 404 : 503;
226
+ }
227
+ /**
228
+ * WPGraphQL's GMT time ("2026-10-02T09:00:00", without a zone) in ms, or null
229
+ * when it is absent or not a time.
230
+ */
231
+ function cmsTime(value) {
232
+ if (!value) return null;
233
+ const time = Date.parse(/(Z|[+-]\d\d:?\d\d)$/.test(value) ? value : `${value}Z`);
234
+ return Number.isNaN(time) ? null : time;
235
+ }
236
+ /**
237
+ * An entry's URI as a path. A CMS whose permalinks point at the Frontend gives
238
+ * absolute URIs (`https://acme.example/team/`); only the path is the route.
239
+ * Percent-encoding and the trailing slash are kept as WordPress gives them.
240
+ */
241
+ function uriPath(uri) {
242
+ try {
243
+ return new URL(uri, "https://frontend.invalid").pathname;
244
+ } catch {
245
+ return uri;
246
+ }
247
+ }
248
+ function menuItemHref(url, internalOrigins) {
249
+ try {
250
+ const parsed = new URL(url, internalOrigins[0]);
251
+ if (!internalOrigins.includes(parsed.origin)) return url;
252
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
253
+ } catch {
254
+ return url;
255
+ }
256
+ }
257
+ function stripHtml(value) {
258
+ return value.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
259
+ }
260
+ function formatDate(value, lang = "en") {
261
+ return new Intl.DateTimeFormat(lang, {
262
+ day: "numeric",
263
+ month: "long",
264
+ year: "numeric"
265
+ }).format(new Date(value));
266
+ }
267
+ function routeOf(path) {
268
+ let route = path;
269
+ try {
270
+ route = decodeURI(path);
271
+ } catch {}
272
+ if (!route.startsWith("/")) route = `/${route}`;
273
+ return route.endsWith("/") ? route : `${route}/`;
274
+ }
275
+ //#endregion
276
+ export { youtubeVideoId as _, routeOf as a, htmlLang as c, routeLanguage as d, siteLanguages as f, renderWordPressBlocks as g, parseWordPressBlocks as h, responseStatus as i, isLanguageHome as l, hasVideoHero as m, formatDate as n, stripHtml as o, VIDEO_HERO_BLOCK as p, menuItemHref as r, uriPath as s, cmsTime as t, languageSubject as u, containerChildLayoutHtml as v, containerLayoutHtml as y };
@@ -0,0 +1,75 @@
1
+ import { A as PublishedEntry, B as htmlLang, C as Delivery, D as Found, E as Extended, F as WordPressMenuItem, H as languageSubject, I as WordPressPost, L as WordPressSettings, M as Translation, N as Unavailable, O as HomeContent, P as WordPressImage, R as SiteLanguage, S as CmsFailureReason, T as EntryContent, U as routeLanguage, V as isLanguageHome, W as siteLanguages, _ as NotServed, a as ContentRuntime, b as SqlStatement, c as Alternate, d as EntryPage, f as EntryPublication, g as Moved, h as HomePublication, i as ContentExtension, j as SiteChrome, k as Missing, l as DeliveryFailureReason, m as HomePage, n as ContentConfig, o as ExtensionNode, p as HomeDelivery, r as ContentConsentConfig, t as WordPressClient, u as EntryDelivery, v as ContentBindings, w as DesignPresets, x as CmsFailure, y as SqlDatabase, z as WordPressLanguages } from "./wordpress-CUFb6YZZ.js";
2
+ //#region frontend/src/wp-block-styles.d.ts
3
+ export interface Preset {
4
+ slug: string;
5
+ }
6
+ export type SpacingPreset = Preset & {
7
+ size: string;
8
+ };
9
+ export type ColorPreset = Preset & {
10
+ color: string;
11
+ };
12
+ export interface BlockPresetStyles {
13
+ vars: Record<string, string>;
14
+ colorRules: string;
15
+ }
16
+ /**
17
+ * WordPress preset classes need the CMS palette as well as CSS variables.
18
+ * `scope` is the content's container (`.prose-wp` by default; another for
19
+ * content a Site renders itself).
20
+ */
21
+ export declare function blockPresetStyles(spacingSizes: SpacingPreset[], colors: ColorPreset[], scope?: string): BlockPresetStyles;
22
+ /**
23
+ * The design presets as WordPress defines them, on `:root`, for styles that
24
+ * read the palette and spacing from these variables outside the content's
25
+ * container. Presets with an invalid slug, or a value that could end the
26
+ * rule, are left out; "" without any.
27
+ */
28
+ export declare function presetRootStyle(spacingSizes: SpacingPreset[], colors: ColorPreset[]): string;
29
+ //#endregion
30
+ //#region frontend/src/wp-block-renderer.d.ts
31
+ export interface WordPressBlock {
32
+ name: string;
33
+ attributes?: Record<string, unknown>;
34
+ htmlContent?: string;
35
+ /** Complete server-rendered HTML; htmlContent may be only a container shell. */
36
+ dynamicContent?: string;
37
+ innerBlocks?: WordPressBlock[];
38
+ }
39
+ export declare const VIDEO_HERO_BLOCK = "getquick-design/video-hero";
40
+ export declare function parseWordPressBlocks(value: unknown): WordPressBlock[];
41
+ export declare function hasVideoHero(blocks: WordPressBlock[]): boolean;
42
+ export declare function youtubeVideoId(value: string): string;
43
+ export declare function renderWordPressBlocks(blocks: WordPressBlock[]): string;
44
+ //#endregion
45
+ //#region frontend/src/wp-container-layout.d.ts
46
+ export type ContainerAttributes = Record<string, unknown>;
47
+ /** WPGraphQL renders nested blocks independently. Restore the parent's resolved
48
+ * variables on direct container children without replacing their own layout. */
49
+ export declare function containerChildLayoutHtml(html: string, parentHtml: string): string;
50
+ /** Rendered CMS shells already carry resolved theme defaults. Saved shells need
51
+ * the same opt-in adapter; absent defaults deliberately fall back to no width.
52
+ */
53
+ export declare function containerLayoutHtml(html: string, attributes: ContainerAttributes): string;
54
+ //#endregion
55
+ //#region frontend/src/formatting.d.ts
56
+ export declare function responseStatus(delivery: {
57
+ kind: Delivery<unknown>["kind"];
58
+ }): 200 | 404 | 503;
59
+ /**
60
+ * WPGraphQL's GMT time ("2026-10-02T09:00:00", without a zone) in ms, or null
61
+ * when it is absent or not a time.
62
+ */
63
+ export declare function cmsTime(value: string | null | undefined): number | null;
64
+ /**
65
+ * An entry's URI as a path. A CMS whose permalinks point at the Frontend gives
66
+ * absolute URIs (`https://acme.example/team/`); only the path is the route.
67
+ * Percent-encoding and the trailing slash are kept as WordPress gives them.
68
+ */
69
+ export declare function uriPath(uri: string): string;
70
+ export declare function menuItemHref(url: string, internalOrigins: string[]): string;
71
+ export declare function stripHtml(value: string): string;
72
+ export declare function formatDate(value: string, lang?: string): string;
73
+ export declare function routeOf(path: string): string;
74
+ //#endregion
75
+ export { Alternate, CmsFailure, CmsFailureReason, ContentBindings, type ContentConfig, type ContentConsentConfig, type ContentExtension, type ContentRuntime, Delivery, DeliveryFailureReason, DesignPresets, EntryContent, EntryDelivery, EntryPage, EntryPublication, Extended, type ExtensionNode, Found, HomeContent, HomeDelivery, HomePage, HomePublication, Missing, Moved, NotServed, PublishedEntry, SiteChrome, SiteLanguage, SqlDatabase, SqlStatement, Translation, Unavailable, type WordPressClient, WordPressImage, WordPressLanguages, WordPressMenuItem, WordPressPost, WordPressSettings, htmlLang, isLanguageHome, languageSubject, routeLanguage, siteLanguages };
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ import { _ as youtubeVideoId, a as routeOf, c as htmlLang, d as routeLanguage, f as siteLanguages, g as renderWordPressBlocks, h as parseWordPressBlocks, i as responseStatus, l as isLanguageHome, m as hasVideoHero, n as formatDate, o as stripHtml, p as VIDEO_HERO_BLOCK, r as menuItemHref, s as uriPath, t as cmsTime, u as languageSubject, v as containerChildLayoutHtml, y as containerLayoutHtml } from "./formatting-wOk2jz6J.js";
2
+ //#region frontend/src/wp-block-styles.ts
3
+ const validSlug = (slug) => /^[a-z0-9][a-z0-9-]*$/.test(slug);
4
+ /** A CSS value safe inside a stylesheet: a color or length, nothing that ends a rule. */
5
+ const safeValue = (value) => /^[#(),.%\w\s-]+$/.test(value);
6
+ /** A selector for the content's container: classes, ids, elements and descendants. */
7
+ const validScope = (scope) => /^[.#]?[A-Za-z][\w-]*(?:[ .#>][A-Za-z][\w-]*)*$/.test(scope);
8
+ /**
9
+ * WordPress preset classes need the CMS palette as well as CSS variables.
10
+ * `scope` is the content's container (`.prose-wp` by default; another for
11
+ * content a Site renders itself).
12
+ */
13
+ function blockPresetStyles(spacingSizes, colors, scope = ".prose-wp") {
14
+ if (!validScope(scope)) throw new Error(`${JSON.stringify(scope)} isn't a container selector`);
15
+ const vars = {};
16
+ const colorRules = [];
17
+ for (const { slug, size } of spacingSizes) if (validSlug(slug)) vars[`--wp--preset--spacing--${slug}`] = size;
18
+ for (const { slug, color } of colors) {
19
+ if (!validSlug(slug)) continue;
20
+ vars[`--wp--preset--color--${slug}`] = color;
21
+ colorRules.push(`${scope} .has-${slug}-color{color:var(--wp--preset--color--${slug})}`, `${scope} .has-${slug}-background-color{background-color:var(--wp--preset--color--${slug})}`, `${scope} .has-${slug}-color.has-link-color a{color:var(--wp--preset--color--${slug})}`);
22
+ }
23
+ return {
24
+ vars,
25
+ colorRules: colorRules.join("\n")
26
+ };
27
+ }
28
+ /**
29
+ * The design presets as WordPress defines them, on `:root`, for styles that
30
+ * read the palette and spacing from these variables outside the content's
31
+ * container. Presets with an invalid slug, or a value that could end the
32
+ * rule, are left out; "" without any.
33
+ */
34
+ function presetRootStyle(spacingSizes, colors) {
35
+ const declarations = [...spacingSizes.filter(({ slug, size }) => validSlug(slug) && safeValue(size)).map(({ slug, size }) => `--wp--preset--spacing--${slug}:${size}`), ...colors.filter(({ slug, color }) => validSlug(slug) && safeValue(color)).map(({ slug, color }) => `--wp--preset--color--${slug}:${color}`)];
36
+ return declarations.length ? `:root{${declarations.join(";")}}` : "";
37
+ }
38
+ //#endregion
39
+ export { VIDEO_HERO_BLOCK, blockPresetStyles, cmsTime, containerChildLayoutHtml, containerLayoutHtml, formatDate, hasVideoHero, htmlLang, isLanguageHome, languageSubject, menuItemHref, parseWordPressBlocks, presetRootStyle, renderWordPressBlocks, responseStatus, routeLanguage, routeOf, siteLanguages, stripHtml, uriPath, youtubeVideoId };
@@ -0,0 +1,48 @@
1
+ //#region frontend/src/consent/legal.d.ts
2
+ /**
3
+ * LEGAL-1's Legal Contract, as gq-config publishes it (fixed in the consent
4
+ * execution record): per language, each assigned role's public URL, title and
5
+ * revision, plus the controller's identity.
6
+ */
7
+ declare const LEGAL_ROLES: readonly ["privacy_policy", "cookie_policy", "terms_of_use", "legal_notice", "terms_of_sale", "returns_policy", "shipping_policy", "complaints_book", "dispute_resolution"];
8
+ /** The roles every Site needs; commerce and Portuguese-consumer roles come from gq-config's `missingRequired`. */
9
+ declare const REQUIRED_LEGAL_ROLES: readonly ["privacy_policy", "cookie_policy"];
10
+ interface LegalRole {
11
+ role: string;
12
+ url: string;
13
+ title: string | null;
14
+ revision: string | null;
15
+ isFallback: boolean;
16
+ }
17
+ interface LegalController {
18
+ name: string | null;
19
+ privacyEmail: string | null;
20
+ dpoContact: string | null;
21
+ }
22
+ /** One language's part of the Contract: what a content Site stores in that language's chrome. */
23
+ interface LegalLanguage {
24
+ controller: LegalController;
25
+ roles: LegalRole[];
26
+ missingRequired: string[];
27
+ }
28
+ interface LegalContract {
29
+ schemaVersion: 1;
30
+ controller: LegalController;
31
+ languages: Record<string, {
32
+ roles: LegalRole[];
33
+ }>;
34
+ missingRequired: string[];
35
+ }
36
+ /**
37
+ * One language's Contract from untrusted input (GraphQL or the storefront
38
+ * payload): unknown roles, unsafe URLs and malformed entries are dropped.
39
+ */
40
+ declare function parseLegalLanguage(value: unknown): LegalLanguage | null;
41
+ /**
42
+ * The LEGAL-1 revisions in force on a page, recorded with each decision
43
+ * (CN-R7). A revision the record schema can't hold is left out rather than
44
+ * making every record invalid.
45
+ */
46
+ declare function legalRevisions(legal: LegalLanguage | null | undefined): Record<string, string>;
47
+ //#endregion
48
+ export { REQUIRED_LEGAL_ROLES as a, LegalRole as i, LegalContract as n, legalRevisions as o, LegalLanguage as r, parseLegalLanguage as s, LEGAL_ROLES as t };
@@ -0,0 +1,4 @@
1
+ //#region frontend/src/migrations.d.ts
2
+ /** Infrastructure only: SQL shipped alongside the installed package. */
3
+ export declare const migrationsDirectory: string;
4
+ //#endregion
@@ -0,0 +1,6 @@
1
+ import { fileURLToPath } from "node:url";
2
+ //#region frontend/src/migrations.ts
3
+ /** Infrastructure only: SQL shipped alongside the installed package. */
4
+ const migrationsDirectory = fileURLToPath(new URL("../migrations/", import.meta.url));
5
+ //#endregion
6
+ export { migrationsDirectory };
@@ -0,0 +1,5 @@
1
+ //#region frontend/src/consent/paths.ts
2
+ /** The same-origin consent endpoint on the Site's Frontend Worker (fixed interface). */
3
+ const CONSENT_PATH = "/gq/consent";
4
+ //#endregion
5
+ export { CONSENT_PATH as t };
@@ -0,0 +1,56 @@
1
+ import { i as ConsentAction, o as ConsentDecisions, s as ConsentState } from "./state-Dzd52F3c.js";
2
+ //#region frontend/src/consent/records.d.ts
3
+ /**
4
+ * A stored consent record (CN-R7), as the read endpoint returns it. No IP
5
+ * address, user agent or account link exists in it.
6
+ */
7
+ interface ConsentRecord {
8
+ consentId: string;
9
+ siteId: string;
10
+ /** Server UTC timestamp (ISO 8601). */
11
+ recordedAt: string;
12
+ configVersion: string;
13
+ /** LEGAL-1 page revisions in force: role → revision. */
14
+ legalRevisions: Record<string, string>;
15
+ decisions: ConsentDecisions;
16
+ action: ConsentAction;
17
+ /** The banner language (BCP 47). */
18
+ language: string;
19
+ uiVersion: string;
20
+ }
21
+ /**
22
+ * What the browser posts to `POST /gq/consent`. The server adds the Site ID and
23
+ * its own timestamp. `decidedAt` identifies the decision, so a retried post
24
+ * appends nothing twice.
25
+ */
26
+ interface ConsentRecordRequest {
27
+ consentId: string;
28
+ decidedAt: string;
29
+ configVersion: string;
30
+ legalRevisions: Record<string, string>;
31
+ decisions: ConsentDecisions;
32
+ action: ConsentAction;
33
+ language: string;
34
+ uiVersion: string;
35
+ }
36
+ /** The largest record body the endpoint accepts (CN-R9). */
37
+ declare const MAX_RECORD_BYTES = 2048;
38
+ declare function consentRecordRequest(state: ConsentState, action: ConsentAction, context: {
39
+ language: string;
40
+ uiVersion: string;
41
+ legalRevisions?: Record<string, string>;
42
+ }): ConsentRecordRequest;
43
+ /** Whether a value can be recorded as a legal role (the record schema's rule). */
44
+ declare const isLegalRoleName: (value: unknown) => value is string;
45
+ /** Whether a value can be recorded as a revision or UI version: a short token. */
46
+ declare const isRecordToken: (value: unknown) => value is string;
47
+ /** A strict parse of a record request: exactly its fields, each bounded. */
48
+ declare function parseConsentRecordRequest(value: unknown): {
49
+ ok: true;
50
+ request: ConsentRecordRequest;
51
+ } | {
52
+ ok: false;
53
+ error: string;
54
+ };
55
+ //#endregion
56
+ export { isLegalRoleName as a, consentRecordRequest as i, ConsentRecordRequest as n, isRecordToken as o, MAX_RECORD_BYTES as r, parseConsentRecordRequest as s, ConsentRecord as t };
@@ -0,0 +1,74 @@
1
+ import { l as isConsentId, t as CONSENT_ACTIONS, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
2
+ //#region frontend/src/consent/records.ts
3
+ /** The largest record body the endpoint accepts (CN-R9). */
4
+ const MAX_RECORD_BYTES = 2048;
5
+ function consentRecordRequest(state, action, context) {
6
+ return {
7
+ consentId: state.consentId,
8
+ decidedAt: new Date(state.decidedAt).toISOString(),
9
+ configVersion: state.version,
10
+ legalRevisions: context.legalRevisions ?? {},
11
+ decisions: { ...state.decisions },
12
+ action,
13
+ language: context.language,
14
+ uiVersion: context.uiVersion
15
+ };
16
+ }
17
+ const KEYS = [
18
+ "consentId",
19
+ "decidedAt",
20
+ "configVersion",
21
+ "legalRevisions",
22
+ "decisions",
23
+ "action",
24
+ "language",
25
+ "uiVersion"
26
+ ];
27
+ const TOKEN = /^[A-Za-z0-9._:@/+-]{1,64}$/;
28
+ const ROLE = /^[a-z_]{1,32}$/;
29
+ /** Whether a value can be recorded as a legal role (the record schema's rule). */
30
+ const isLegalRoleName = (value) => typeof value === "string" && ROLE.test(value);
31
+ /** Whether a value can be recorded as a revision or UI version: a short token. */
32
+ const isRecordToken = (value) => typeof value === "string" && TOKEN.test(value);
33
+ const LANGUAGE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,2}$/;
34
+ const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
35
+ /** A strict parse of a record request: exactly its fields, each bounded. */
36
+ function parseConsentRecordRequest(value) {
37
+ const fail = (error) => ({
38
+ ok: false,
39
+ error
40
+ });
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail("not an object");
42
+ const body = value;
43
+ const extra = Object.keys(body).filter((key) => !KEYS.includes(key));
44
+ if (extra.length) return fail(`unexpected fields: ${extra.join(", ")}`);
45
+ if (!isConsentId(body.consentId)) return fail("consentId must be a UUID v4");
46
+ if (typeof body.decidedAt !== "string" || !ISO.test(body.decidedAt)) return fail("decidedAt must be an ISO 8601 UTC time");
47
+ if (typeof body.configVersion !== "string" || !/^[0-9a-f]{8,64}$/.test(body.configVersion)) return fail("configVersion must be a hex hash");
48
+ if (typeof body.action !== "string" || !CONSENT_ACTIONS.includes(body.action)) return fail("action is not a consent action");
49
+ if (typeof body.language !== "string" || !LANGUAGE.test(body.language)) return fail("language must be a BCP 47 tag");
50
+ if (typeof body.uiVersion !== "string" || !TOKEN.test(body.uiVersion)) return fail("uiVersion must be a short token");
51
+ const decisions = body.decisions;
52
+ if (!decisions || typeof decisions !== "object" || Array.isArray(decisions)) return fail("decisions must be an object");
53
+ if (Object.keys(decisions).length !== OPTIONAL_CATEGORIES.length || !OPTIONAL_CATEGORIES.every((category) => typeof decisions[category] === "boolean")) return fail("decisions must have exactly the four optional categories, as booleans");
54
+ const revisions = body.legalRevisions;
55
+ if (!revisions || typeof revisions !== "object" || Array.isArray(revisions)) return fail("legalRevisions must be an object");
56
+ const roles = Object.entries(revisions);
57
+ if (roles.length > 16) return fail("too many legal revisions");
58
+ for (const [role, revision] of roles) if (!isLegalRoleName(role) || !isRecordToken(revision)) return fail("legalRevisions must map roles to short revision tokens");
59
+ return {
60
+ ok: true,
61
+ request: {
62
+ consentId: body.consentId,
63
+ decidedAt: body.decidedAt,
64
+ configVersion: body.configVersion,
65
+ legalRevisions: Object.fromEntries(roles),
66
+ decisions: Object.fromEntries(OPTIONAL_CATEGORIES.map((category) => [category, decisions[category]])),
67
+ action: body.action,
68
+ language: body.language,
69
+ uiVersion: body.uiVersion
70
+ }
71
+ };
72
+ }
73
+ //#endregion
74
+ export { parseConsentRecordRequest as a, isRecordToken as i, consentRecordRequest as n, isLegalRoleName as r, MAX_RECORD_BYTES as t };
@@ -0,0 +1,2 @@
1
+ import { a as ContentRuntime, i as ContentExtension, n as ContentConfig, o as ExtensionNode, r as ContentConsentConfig, s as createContentRuntime, t as WordPressClient, v as ContentBindings } from "./wordpress-CUFb6YZZ.js";
2
+ export { type ContentBindings, ContentConfig, ContentConsentConfig, ContentExtension, ContentRuntime, ExtensionNode, type WordPressClient, createContentRuntime };