@getquick/content 0.3.2 → 0.4.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.
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @getquick/content
2
2
 
3
+ The runtime's public `search({ search, page?, perPage?, language? })` searches
4
+ delivered pages, posts, declared collections and the language's front page
5
+ from the publication store alone. It returns a `found.content` page with
6
+ `items`, `total`, `page`, `perPage`, `totalPages`, or `unavailable.failure`.
7
+ See the [search contract and adoption guide](../docs/search.md).
8
+
3
9
  The configured content runtime for WordPress HTML and WPGraphQL. This slice
4
10
  provides pure helpers, the configured WordPress client, durable delivery and
5
11
  installed SQL migrations, and event/refresh HTTP handlers. It is released as a
@@ -1,6 +1,6 @@
1
1
  import { S as SERVICE_CATALOG, T as copyLanguage, c as grantedCategories, d as promptReason, f as readConsentCookie, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
2
2
  import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-CJJemAmE.js";
3
- import { t as CONSENT_UI_VERSION } from "./version-DkpQm85L.js";
3
+ import { t as CONSENT_UI_VERSION } from "./version-D81MsCZU.js";
4
4
  import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
5
5
  import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
6
6
  //#region frontend/src/consent/astro.ts
@@ -1,7 +1,8 @@
1
1
  import { t as bearerMatches } from "./bearer-DMOCoSci.js";
2
2
  import { i as DECISION_VALIDITY_MS, l as isConsentId, t as CONSENT_ACTIONS, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
3
- import { a as parseConsentRecordRequest } from "./records-Dumrz759.js";
3
+ import { a as parseConsentRecordRequest, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
4
4
  import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
5
+ import { n as readBoundedBody, t as isSameOrigin } from "./request-guards-BUPr_vA4.js";
5
6
  //#region frontend/src/consent/server.ts
6
7
  const MAX_RETENTION_DAYS = 1826;
7
8
  const DAY_MS = 864e5;
@@ -73,42 +74,8 @@ function createConsentServer(config) {
73
74
  const now = config.now ?? Date.now;
74
75
  const origins = new Set((config.siteOrigins ?? []).map((origin) => new URL(origin).origin));
75
76
  /** CN-R9: same-origin only. A browser's cross-site post carries its Origin. */
76
- function sameOrigin(request) {
77
- const origin = request.headers.get("Origin");
78
- const site = request.headers.get("Sec-Fetch-Site");
79
- if (site && site !== "same-origin") return false;
80
- if (!origin) return false;
81
- if (origin === new URL(request.url).origin || origins.has(origin)) return true;
82
- try {
83
- return new URL(origin).host === request.headers.get("Host");
84
- } catch {
85
- return false;
86
- }
87
- }
88
- async function readBody(request) {
89
- if (Number(request.headers.get("Content-Length") ?? "0") > 2048) return null;
90
- if (!request.body) return "";
91
- const reader = request.body.getReader();
92
- const chunks = [];
93
- let size = 0;
94
- for (;;) {
95
- const { done, value } = await reader.read();
96
- if (done) break;
97
- size += value.byteLength;
98
- if (size > 2048) {
99
- await reader.cancel();
100
- return null;
101
- }
102
- chunks.push(value);
103
- }
104
- const bytes = new Uint8Array(size);
105
- let offset = 0;
106
- for (const chunk of chunks) {
107
- bytes.set(chunk, offset);
108
- offset += chunk.byteLength;
109
- }
110
- return new TextDecoder().decode(bytes);
111
- }
77
+ const sameOrigin = (request) => isSameOrigin(request, origins);
78
+ const readBody = (request) => readBoundedBody(request, MAX_RECORD_BYTES);
112
79
  async function insert(db, request) {
113
80
  const recordedAt = now();
114
81
  return (await db.prepare(`INSERT OR IGNORE INTO consent_records (consent_id, site_id, recorded_at, decided_at, expires_at,
package/dist/consent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
2
2
  import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
3
3
  import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-CJJemAmE.js";
4
- import { t as CONSENT_UI_VERSION } from "./version-DkpQm85L.js";
4
+ import { t as CONSENT_UI_VERSION } from "./version-D81MsCZU.js";
5
5
  export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };
@@ -0,0 +1,163 @@
1
+ //#region frontend/src/forms/state.ts
2
+ /** The query parameter that marks the redirect back after a post. */
3
+ const FORM_STATE_PARAM = "gq-form";
4
+ /** Seconds the outcome is kept for the page after the redirect. */
5
+ const FORM_STATE_MAX_AGE = 300;
6
+ /** Cookies over this many bytes drop the typed values, then Gravity Forms' messages. */
7
+ const MAX_COOKIE_BYTES = 3800;
8
+ const formStateCookieName = (formId) => `gq-form-${formId}`;
9
+ function encode(value) {
10
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
11
+ let binary = "";
12
+ for (const byte of bytes) binary += String.fromCharCode(byte);
13
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
14
+ }
15
+ function decode(value) {
16
+ const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/"));
17
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0))));
18
+ }
19
+ const STATUSES = /* @__PURE__ */ new Set([
20
+ "sent",
21
+ "invalid",
22
+ "failed",
23
+ "verification",
24
+ "too-many"
25
+ ]);
26
+ /** The Set-Cookie header that carries a form's outcome to the page. */
27
+ function formStateCookie(state, options) {
28
+ const attempts = [
29
+ {
30
+ s: state.status,
31
+ e: state.errors,
32
+ m: state.messages,
33
+ v: state.values,
34
+ c: state.confirmation
35
+ },
36
+ {
37
+ s: state.status,
38
+ e: state.errors,
39
+ m: state.messages,
40
+ c: state.confirmation
41
+ },
42
+ {
43
+ s: state.status,
44
+ e: state.errors
45
+ }
46
+ ];
47
+ let value = "";
48
+ for (const attempt of attempts) {
49
+ value = encode(attempt);
50
+ if (value.length <= MAX_COOKIE_BYTES) break;
51
+ }
52
+ return `${formStateCookieName(state.formId)}=${value}; Path=/; Max-Age=300; HttpOnly; SameSite=Lax` + (options.secure ? "; Secure" : "");
53
+ }
54
+ /** The Set-Cookie header that removes a form's outcome once the page has shown it. */
55
+ function clearFormStateCookie(formId, options) {
56
+ return `${formStateCookieName(formId)}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax` + (options.secure ? "; Secure" : "");
57
+ }
58
+ function cookieValue(header, name) {
59
+ for (const part of (header ?? "").split(";")) {
60
+ const index = part.indexOf("=");
61
+ if (index > 0 && part.slice(0, index).trim() === name) return part.slice(index + 1).trim();
62
+ }
63
+ return null;
64
+ }
65
+ const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string")) : {};
66
+ const byField = (value) => Object.fromEntries(Object.entries(stringRecord(value)).flatMap(([key, each]) => /^\d+$/.test(key) ? [[Number(key), each]] : []));
67
+ /**
68
+ * A form's outcome for the page: present only on the redirect back after a
69
+ * post to it (`?gq-form=<id>`), from its cookie. Null otherwise.
70
+ */
71
+ function readFormState(request, formId) {
72
+ let url;
73
+ try {
74
+ url = new URL(request.url);
75
+ } catch {
76
+ return null;
77
+ }
78
+ if (url.searchParams.get("gq-form") !== String(formId)) return null;
79
+ const raw = cookieValue(request.cookie, formStateCookieName(formId));
80
+ if (!raw) return null;
81
+ let value;
82
+ try {
83
+ value = decode(raw);
84
+ } catch {
85
+ return null;
86
+ }
87
+ if (typeof value !== "object" || value === null) return null;
88
+ const record = value;
89
+ if (!STATUSES.has(record.s)) return null;
90
+ return {
91
+ formId,
92
+ status: record.s,
93
+ errors: byField(record.e),
94
+ messages: byField(record.m),
95
+ values: stringRecord(record.v),
96
+ ...typeof record.c === "string" && record.c ? { confirmation: record.c } : {}
97
+ };
98
+ }
99
+ /** Where the handler sends the visitor back to: the page, marked for the form. */
100
+ function formStateLocation(source, formId) {
101
+ const url = new URL(source, "https://site.invalid");
102
+ url.searchParams.set(FORM_STATE_PARAM, String(formId));
103
+ url.hash = `gq-form-${formId}`;
104
+ return `${url.pathname}${url.search}${url.hash}`;
105
+ }
106
+ //#endregion
107
+ //#region frontend/src/forms/copy.ts
108
+ const FORMS_COPY = {
109
+ en: {
110
+ required: "required",
111
+ requiredFields: "Fields marked with * are required.",
112
+ errorSummary: "There was a problem with your submission. Please review the fields below.",
113
+ errors: {
114
+ required: "This field is required.",
115
+ email: "Enter a valid email address.",
116
+ "email-mismatch": "The email addresses don't match.",
117
+ number: "Enter a number.",
118
+ range: "Enter a number in the allowed range.",
119
+ url: "Enter a valid web address, starting with https://.",
120
+ choice: "Choose one of the options.",
121
+ "too-long": "This is longer than allowed."
122
+ },
123
+ failed: "Your message couldn't be sent. Please try again later.",
124
+ verification: "We couldn't verify that you're not a robot. Please try again.",
125
+ tooMany: "Too many submissions. Please try again later.",
126
+ sent: "Thank you. Your message has been sent.",
127
+ unavailable: "This form isn't available.",
128
+ submit: "Submit",
129
+ privacyPolicy: "Privacy policy",
130
+ honeypot: "This field is for validation purposes and should be left unchanged.",
131
+ needsJavaScript: "This form needs JavaScript to verify you're not a robot."
132
+ },
133
+ "pt-PT": {
134
+ required: "obrigatório",
135
+ requiredFields: "Os campos assinalados com * são obrigatórios.",
136
+ errorSummary: "Houve um problema com o envio. Reveja os campos abaixo.",
137
+ errors: {
138
+ required: "Este campo é obrigatório.",
139
+ email: "Introduza um endereço de email válido.",
140
+ "email-mismatch": "Os endereços de email não coincidem.",
141
+ number: "Introduza um número.",
142
+ range: "Introduza um número dentro do intervalo permitido.",
143
+ url: "Introduza um endereço web válido, começado por https://.",
144
+ choice: "Escolha uma das opções.",
145
+ "too-long": "Excede o comprimento permitido."
146
+ },
147
+ failed: "Não foi possível enviar a sua mensagem. Tente novamente mais tarde.",
148
+ verification: "Não foi possível verificar que não é um robô. Tente novamente.",
149
+ tooMany: "Demasiados envios. Tente novamente mais tarde.",
150
+ sent: "Obrigado. A sua mensagem foi enviada.",
151
+ unavailable: "Este formulário não está disponível.",
152
+ submit: "Enviar",
153
+ privacyPolicy: "Política de privacidade",
154
+ honeypot: "Este campo serve para validação e deve ser deixado em branco.",
155
+ needsJavaScript: "Este formulário precisa de JavaScript para verificar que não é um robô."
156
+ }
157
+ };
158
+ /** The copy language for a page's `lang`: Portuguese for `pt*`, else English. */
159
+ function formsCopyLanguage(language) {
160
+ return /^pt\b/i.test(language ?? "") ? "pt-PT" : "en";
161
+ }
162
+ //#endregion
163
+ export { clearFormStateCookie as a, formStateLocation as c, FORM_STATE_PARAM as i, readFormState as l, formsCopyLanguage as n, formStateCookie as o, FORM_STATE_MAX_AGE as r, formStateCookieName as s, FORMS_COPY as t };
@@ -0,0 +1,143 @@
1
+ //#region frontend/src/forms/descriptor.d.ts
2
+ /** The Site path the forms handler answers under: `/gq/forms/<formId>`. */
3
+ declare const FORMS_PATH = "/gq/forms";
4
+ /** The descriptor's wire version; another version doesn't parse. */
5
+ declare const FORM_DESCRIPTOR_SCHEMA = 1;
6
+ /** The Gravity Forms block a Site's dispatcher routes to the renderer. */
7
+ declare const GRAVITY_FORMS_BLOCK = "gravityforms/form";
8
+ /** Field types this release renders and submits. */
9
+ declare const SUPPORTED_FIELD_TYPES: readonly ["text", "textarea", "email", "phone", "number", "website", "select", "radio", "checkbox", "consent", "name", "hidden", "html", "section"];
10
+ type FormFieldType = (typeof SUPPORTED_FIELD_TYPES)[number];
11
+ interface FormChoice {
12
+ text: string;
13
+ value: string;
14
+ selected: boolean;
15
+ }
16
+ /** One HTML input of a field: Gravity Forms' `input_<field>` or `input_<field>_<n>`. */
17
+ interface FormInput {
18
+ /** Gravity Forms' input id (`1`, `1.3`). */
19
+ id: string;
20
+ /** The name it is posted as. */
21
+ name: string;
22
+ label: string;
23
+ /** For a checkbox input, the choice's value. */
24
+ value?: string;
25
+ placeholder?: string;
26
+ /** Name sub-inputs: autocomplete token (given-name, family-name…). */
27
+ autocomplete?: string;
28
+ }
29
+ interface FormConsent {
30
+ /** The text next to the checkbox. */
31
+ checkboxLabel: string;
32
+ /** The consent's longer text, as plain text. */
33
+ description: string;
34
+ /** Gravity Forms' form revision the text belongs to; submitted with the consent. */
35
+ revisionId: string;
36
+ }
37
+ interface FormField {
38
+ id: number;
39
+ type: FormFieldType;
40
+ label: string;
41
+ /** Plain text. */
42
+ description: string;
43
+ required: boolean;
44
+ /** A hidden field is submitted, not shown; a `hidden` visibility field is too. */
45
+ visibility: "visible" | "hidden";
46
+ placeholder?: string;
47
+ maxLength?: number;
48
+ defaultValue?: string;
49
+ /** select, radio, checkbox. */
50
+ choices?: FormChoice[];
51
+ /** The inputs it posts (one for most fields). */
52
+ inputs: FormInput[];
53
+ /** consent only. */
54
+ consent?: FormConsent;
55
+ /** html only: sanitized by WordPress (wp_kses_post) on the CMS. */
56
+ content?: string;
57
+ /** number only. */
58
+ range?: {
59
+ min?: number;
60
+ max?: number;
61
+ };
62
+ /** The Site's CSS classes the editor set on the field. */
63
+ cssClass?: string;
64
+ }
65
+ type FormConfirmation = {
66
+ type: "message";
67
+ message: string;
68
+ } | {
69
+ type: "redirect";
70
+ url: string;
71
+ };
72
+ /** Why a form can't be offered as it is; readiness reports each one as a failure. */
73
+ type FormDiagnosticCode = "unavailable" | "unsupported-captcha" | "unsupported-file-upload" | "unsupported-payment" | "unsupported-multi-page" | "unsupported-field" | "unsupported-conditional-logic" | "requires-login";
74
+ interface FormDiagnostic {
75
+ code: FormDiagnosticCode;
76
+ fieldId?: number;
77
+ /** Gravity Forms' field type, for a field diagnostic. */
78
+ type?: string;
79
+ message: string;
80
+ }
81
+ interface FormDescriptor {
82
+ schema: typeof FORM_DESCRIPTOR_SCHEMA;
83
+ id: number;
84
+ /** False when the form is missing, inactive or trashed: it renders as unavailable. */
85
+ available: boolean;
86
+ title: string;
87
+ /** Plain text. */
88
+ description: string;
89
+ submitLabel: string;
90
+ fields: FormField[];
91
+ /** Gravity Forms' honeypot input, when the form enables it. */
92
+ honeypot: {
93
+ name: string;
94
+ } | null;
95
+ /** The default confirmation. */
96
+ confirmation: FormConfirmation;
97
+ /** Set by the CMS for fields and settings this release doesn't support. */
98
+ diagnostics: FormDiagnostic[];
99
+ }
100
+ /**
101
+ * A descriptor as the CMS delivered it, keeping only the public fields above,
102
+ * or null when it isn't one. Fields of a type this release doesn't support
103
+ * are left out; the CMS reports them as diagnostics.
104
+ */
105
+ declare function parseFormDescriptor(value: unknown): FormDescriptor | null;
106
+ /**
107
+ * The descriptors a publication carries (the CMS's `getquickForms`, as JSON
108
+ * text or data), dropping any that don't parse. Undefined for no value.
109
+ */
110
+ declare function parseFormDescriptors(value: unknown): FormDescriptor[] | undefined;
111
+ /** The form a `gravityforms/form` block shows, from its attributes (`formId`). */
112
+ declare function blockFormId(attributes: Record<string, unknown> | null | undefined): number | null;
113
+ /** A form's descriptor among a publication's, or null. */
114
+ declare function findForm(forms: readonly FormDescriptor[] | null | undefined, formId: number): FormDescriptor | null;
115
+ /** The path a form posts to on the Site's own origin. */
116
+ declare const formAction: (formId: number) => string;
117
+ /** Everything readiness must fail for: an unavailable form, and each unsupported field or setting. */
118
+ declare function formDiagnostics(descriptor: FormDescriptor): FormDiagnostic[];
119
+ /** The spam controls a Site's forms use. */
120
+ type FormSpamControl = "honeypot" | "turnstile";
121
+ interface FormsReadiness {
122
+ ok: boolean;
123
+ forms: Array<{
124
+ id: number;
125
+ title: string;
126
+ ok: boolean;
127
+ diagnostics: FormDiagnostic[];
128
+ /** Gravity Forms' honeypot is enabled on the form. */
129
+ honeypot: boolean;
130
+ }>;
131
+ /** Controls the Site declares that no checked form uses (honeypot only). */
132
+ warnings: string[];
133
+ }
134
+ /**
135
+ * Readiness over the forms a Site's published content references: fails for
136
+ * any diagnostic. `spam` is the Site's declared controls; declaring the
137
+ * honeypot for a form that doesn't enable it is reported as a warning.
138
+ */
139
+ declare function formsReadiness(descriptors: readonly FormDescriptor[], options?: {
140
+ spam?: readonly FormSpamControl[];
141
+ }): FormsReadiness;
142
+ //#endregion
143
+ export { parseFormDescriptors as S, findForm as _, FormConsent as a, formsReadiness as b, FormDiagnosticCode as c, FormInput as d, FormSpamControl as f, blockFormId as g, SUPPORTED_FIELD_TYPES as h, FormConfirmation as i, FormField as l, GRAVITY_FORMS_BLOCK as m, FORM_DESCRIPTOR_SCHEMA as n, FormDescriptor as o, FormsReadiness as p, FormChoice as r, FormDiagnostic as s, FORMS_PATH as t, FormFieldType as u, formAction as v, parseFormDescriptor as x, formDiagnostics as y };
@@ -0,0 +1,205 @@
1
+ //#region frontend/src/forms/descriptor.ts
2
+ /** The Site path the forms handler answers under: `/gq/forms/<formId>`. */
3
+ const FORMS_PATH = "/gq/forms";
4
+ /** The descriptor's wire version; another version doesn't parse. */
5
+ const FORM_DESCRIPTOR_SCHEMA = 1;
6
+ /** The Gravity Forms block a Site's dispatcher routes to the renderer. */
7
+ const GRAVITY_FORMS_BLOCK = "gravityforms/form";
8
+ /** Field types this release renders and submits. */
9
+ const SUPPORTED_FIELD_TYPES = [
10
+ "text",
11
+ "textarea",
12
+ "email",
13
+ "phone",
14
+ "number",
15
+ "website",
16
+ "select",
17
+ "radio",
18
+ "checkbox",
19
+ "consent",
20
+ "name",
21
+ "hidden",
22
+ "html",
23
+ "section"
24
+ ];
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ const text = (value) => typeof value === "string" ? value : "";
29
+ const optionalText = (value) => typeof value === "string" && value !== "" ? value : void 0;
30
+ const finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
31
+ const INPUT_NAME = /^input_\d+(?:_\d+)?$/;
32
+ const DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
33
+ "unavailable",
34
+ "unsupported-captcha",
35
+ "unsupported-file-upload",
36
+ "unsupported-payment",
37
+ "unsupported-multi-page",
38
+ "unsupported-field",
39
+ "unsupported-conditional-logic",
40
+ "requires-login"
41
+ ]);
42
+ function parseInput(value) {
43
+ if (!isRecord(value)) return null;
44
+ const id = text(value.id);
45
+ const name = text(value.name);
46
+ if (!/^\d+(?:\.\d+)?$/.test(id) || !INPUT_NAME.test(name)) return null;
47
+ return {
48
+ id,
49
+ name,
50
+ label: text(value.label),
51
+ ...typeof value.value === "string" ? { value: value.value } : {},
52
+ ...optionalText(value.placeholder) ? { placeholder: text(value.placeholder) } : {},
53
+ ...optionalText(value.autocomplete) ? { autocomplete: text(value.autocomplete) } : {}
54
+ };
55
+ }
56
+ function parseField(value) {
57
+ if (!isRecord(value)) return null;
58
+ const id = value.id;
59
+ const type = value.type;
60
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 1) return null;
61
+ if (!SUPPORTED_FIELD_TYPES.includes(type)) return null;
62
+ const inputs = Array.isArray(value.inputs) ? value.inputs.map(parseInput).filter((input) => input !== null) : [];
63
+ const field = {
64
+ id,
65
+ type,
66
+ label: text(value.label),
67
+ description: text(value.description),
68
+ required: value.required === true,
69
+ visibility: value.visibility === "hidden" ? "hidden" : "visible",
70
+ inputs
71
+ };
72
+ if (optionalText(value.placeholder)) field.placeholder = text(value.placeholder);
73
+ const maxLength = finite(value.maxLength);
74
+ if (maxLength !== void 0 && maxLength > 0) field.maxLength = Math.floor(maxLength);
75
+ if (typeof value.defaultValue === "string") field.defaultValue = value.defaultValue;
76
+ if (optionalText(value.cssClass)) field.cssClass = text(value.cssClass);
77
+ if (Array.isArray(value.choices)) field.choices = value.choices.filter(isRecord).map((choice) => ({
78
+ text: text(choice.text),
79
+ value: text(choice.value),
80
+ selected: choice.selected === true
81
+ }));
82
+ if (isRecord(value.consent)) field.consent = {
83
+ checkboxLabel: text(value.consent.checkboxLabel),
84
+ description: text(value.consent.description),
85
+ revisionId: text(value.consent.revisionId)
86
+ };
87
+ if (typeof value.content === "string") field.content = value.content;
88
+ if (isRecord(value.range)) {
89
+ const min = finite(value.range.min);
90
+ const max = finite(value.range.max);
91
+ field.range = {
92
+ ...min !== void 0 ? { min } : {},
93
+ ...max !== void 0 ? { max } : {}
94
+ };
95
+ }
96
+ return field;
97
+ }
98
+ function parseConfirmation(value) {
99
+ if (isRecord(value) && value.type === "redirect" && typeof value.url === "string") try {
100
+ const url = new URL(value.url);
101
+ if (url.protocol === "https:" || url.protocol === "http:") return {
102
+ type: "redirect",
103
+ url: url.href
104
+ };
105
+ } catch {}
106
+ return {
107
+ type: "message",
108
+ message: isRecord(value) ? text(value.message) : ""
109
+ };
110
+ }
111
+ /**
112
+ * A descriptor as the CMS delivered it, keeping only the public fields above,
113
+ * or null when it isn't one. Fields of a type this release doesn't support
114
+ * are left out; the CMS reports them as diagnostics.
115
+ */
116
+ function parseFormDescriptor(value) {
117
+ if (!isRecord(value) || value.schema !== 1) return null;
118
+ const id = value.id;
119
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 1) return null;
120
+ const fields = Array.isArray(value.fields) ? value.fields.map(parseField).filter((field) => field !== null) : [];
121
+ const honeypotName = isRecord(value.honeypot) ? text(value.honeypot.name) : "";
122
+ const diagnostics = Array.isArray(value.diagnostics) ? value.diagnostics.filter(isRecord).flatMap((diagnostic) => {
123
+ const code = diagnostic.code;
124
+ if (!DIAGNOSTIC_CODES.has(code)) return [];
125
+ const fieldId = finite(diagnostic.fieldId);
126
+ return [{
127
+ code,
128
+ ...fieldId !== void 0 ? { fieldId } : {},
129
+ ...optionalText(diagnostic.type) ? { type: text(diagnostic.type) } : {},
130
+ message: text(diagnostic.message)
131
+ }];
132
+ }) : [];
133
+ return {
134
+ schema: 1,
135
+ id,
136
+ available: value.available === true,
137
+ title: text(value.title),
138
+ description: text(value.description),
139
+ submitLabel: text(value.submitLabel),
140
+ fields,
141
+ honeypot: INPUT_NAME.test(honeypotName) ? { name: honeypotName } : null,
142
+ confirmation: parseConfirmation(value.confirmation),
143
+ diagnostics
144
+ };
145
+ }
146
+ /**
147
+ * The descriptors a publication carries (the CMS's `getquickForms`, as JSON
148
+ * text or data), dropping any that don't parse. Undefined for no value.
149
+ */
150
+ function parseFormDescriptors(value) {
151
+ let parsed = value;
152
+ if (typeof parsed === "string") try {
153
+ parsed = JSON.parse(parsed);
154
+ } catch {
155
+ return;
156
+ }
157
+ if (!Array.isArray(parsed)) return void 0;
158
+ return parsed.map(parseFormDescriptor).filter((descriptor) => descriptor !== null);
159
+ }
160
+ /** The form a `gravityforms/form` block shows, from its attributes (`formId`). */
161
+ function blockFormId(attributes) {
162
+ const raw = attributes?.formId;
163
+ const id = typeof raw === "string" ? Number(raw) : raw;
164
+ return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
165
+ }
166
+ /** A form's descriptor among a publication's, or null. */
167
+ function findForm(forms, formId) {
168
+ return forms?.find((form) => form.id === formId) ?? null;
169
+ }
170
+ /** The path a form posts to on the Site's own origin. */
171
+ const formAction = (formId) => `${FORMS_PATH}/${formId}`;
172
+ /** Everything readiness must fail for: an unavailable form, and each unsupported field or setting. */
173
+ function formDiagnostics(descriptor) {
174
+ const diagnostics = [...descriptor.diagnostics];
175
+ if (!descriptor.available && !diagnostics.some((each) => each.code === "unavailable")) diagnostics.unshift({
176
+ code: "unavailable",
177
+ message: `Form ${descriptor.id} is missing, inactive or in the trash.`
178
+ });
179
+ return diagnostics;
180
+ }
181
+ /**
182
+ * Readiness over the forms a Site's published content references: fails for
183
+ * any diagnostic. `spam` is the Site's declared controls; declaring the
184
+ * honeypot for a form that doesn't enable it is reported as a warning.
185
+ */
186
+ function formsReadiness(descriptors, options = {}) {
187
+ const forms = [...new Map(descriptors.map((descriptor) => [descriptor.id, descriptor])).values()].map((descriptor) => {
188
+ const diagnostics = formDiagnostics(descriptor);
189
+ return {
190
+ id: descriptor.id,
191
+ title: descriptor.title,
192
+ ok: diagnostics.length === 0,
193
+ diagnostics,
194
+ honeypot: descriptor.honeypot !== null
195
+ };
196
+ });
197
+ const warnings = options.spam?.includes("honeypot") ? forms.filter((form) => !form.honeypot).map((form) => `Form ${form.id} doesn't enable Gravity Forms' anti-spam honeypot.`) : [];
198
+ return {
199
+ ok: forms.every((form) => form.ok),
200
+ forms,
201
+ warnings
202
+ };
203
+ }
204
+ //#endregion
205
+ export { blockFormId as a, formDiagnostics as c, parseFormDescriptors as d, SUPPORTED_FIELD_TYPES as i, formsReadiness as l, FORM_DESCRIPTOR_SCHEMA as n, findForm as o, GRAVITY_FORMS_BLOCK as r, formAction as s, FORMS_PATH as t, parseFormDescriptor as u };
@@ -0,0 +1,50 @@
1
+ import { _ as findForm, g as blockFormId, l as FormField, m as GRAVITY_FORMS_BLOCK, o as FormDescriptor } from "./descriptor-B-e7gHi1.js";
2
+ import { a as clearFormStateCookie, l as readFormState, n as FORM_STATE_PARAM, r as FormState } from "./state-CsMFLUUO.js";
3
+ //#region frontend/src/forms/astro.d.ts
4
+ /** LEGAL-1's links in the page's language (the chrome's `legal`), structurally. */
5
+ export interface FormsLegal {
6
+ roles: ReadonlyArray<{
7
+ role: string;
8
+ url: string;
9
+ title?: string | null;
10
+ }>;
11
+ }
12
+ export interface GravityFormInput {
13
+ /** The form's descriptor from the publication (`findForm(page.forms, blockFormId(attributes))`). */
14
+ descriptor: FormDescriptor | null;
15
+ /** The page's path: the handler sends the visitor back to it. */
16
+ source: string;
17
+ /** The page's `lang` (BCP 47). */
18
+ language: string;
19
+ /** The page language's Legal Contract: the consent links to its privacy policy. */
20
+ legal?: FormsLegal | null;
21
+ /** The outcome on the redirect back after a post (`readFormState`). */
22
+ state?: FormState | null;
23
+ /** Turnstile's site key, when the Site uses Turnstile. */
24
+ turnstile?: {
25
+ siteKey: string;
26
+ } | null;
27
+ /** The block's `title` and `description` attributes: show the form's title and description. */
28
+ showTitle?: boolean;
29
+ showDescription?: boolean;
30
+ /** The heading level of the form's title (2 by default). */
31
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
32
+ }
33
+ export declare function escapeHtml(value: string): string;
34
+ /** The HTML id of an input (`gq-form-1-input_1_3`). */
35
+ export declare const inputId: (formId: number, name: string) => string;
36
+ /** Turnstile loads only once the visitor interacts with a form (CONSENT-1, CN-R3). */
37
+ export declare const FORMS_TURNSTILE_SCRIPT = "(()=>{const s=\"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\";let l;const load=()=>l??=new Promise((ok,no)=>{const e=document.createElement(\"script\");e.src=s;e.async=true;e.onload=()=>ok(window.turnstile);e.onerror=no;document.head.append(e)});for(const f of document.querySelectorAll(\"form[data-gq-turnstile]\")){const start=()=>{if(f.dataset.gqTurnstileStarted)return;f.dataset.gqTurnstileStarted=\"1\";load().then(t=>t.render(f.querySelector(\"[data-gq-turnstile-widget]\"),{sitekey:f.dataset.gqTurnstile,\"response-field-name\":\"cf-turnstile-response\"}))};f.addEventListener(\"focusin\",start,{once:true});f.addEventListener(\"pointerdown\",start,{once:true})}})();";
38
+ /**
39
+ * A `gravityforms/form` block's HTML. Without a descriptor (the CMS hasn't
40
+ * delivered one) or for an unavailable form, a status message instead of a
41
+ * form that can't be sent.
42
+ */
43
+ export declare function renderGravityForm(input: GravityFormInput): string;
44
+ /**
45
+ * A minimal default stylesheet (layout and states only). Sites usually style
46
+ * the `gq-form*` classes with their own tokens instead.
47
+ */
48
+ export declare const FORMS_STYLES: string;
49
+ //#endregion
50
+ export { FORM_STATE_PARAM, type FormDescriptor, type FormField, type FormState, GRAVITY_FORMS_BLOCK, blockFormId, clearFormStateCookie, findForm, readFormState };