@getquick/content 0.3.1 → 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.
@@ -1574,7 +1574,18 @@ const NUMBERED_HEADERS = /* @__PURE__ */ new Set([
1574
1574
  $.H5,
1575
1575
  $.H6
1576
1576
  ]);
1577
- TAG_NAMES.STYLE, TAG_NAMES.SCRIPT, TAG_NAMES.XMP, TAG_NAMES.IFRAME, TAG_NAMES.NOEMBED, TAG_NAMES.NOFRAMES, TAG_NAMES.PLAINTEXT;
1577
+ const UNESCAPED_TEXT = /* @__PURE__ */ new Set([
1578
+ TAG_NAMES.STYLE,
1579
+ TAG_NAMES.SCRIPT,
1580
+ TAG_NAMES.XMP,
1581
+ TAG_NAMES.IFRAME,
1582
+ TAG_NAMES.NOEMBED,
1583
+ TAG_NAMES.NOFRAMES,
1584
+ TAG_NAMES.PLAINTEXT
1585
+ ]);
1586
+ function hasUnescapedText(tn, scriptingEnabled) {
1587
+ return UNESCAPED_TEXT.has(tn) || scriptingEnabled && tn === TAG_NAMES.NOSCRIPT;
1588
+ }
1578
1589
  //#endregion
1579
1590
  //#region node_modules/.pnpm/parse5@8.0.1/node_modules/parse5/dist/tokenizer/index.js
1580
1591
  var State;
@@ -7010,7 +7021,191 @@ function endTagInForeignContent(p, token) {
7010
7021
  }
7011
7022
  }
7012
7023
  }
7013
- TAG_NAMES.AREA, TAG_NAMES.BASE, TAG_NAMES.BASEFONT, TAG_NAMES.BGSOUND, TAG_NAMES.BR, TAG_NAMES.COL, TAG_NAMES.EMBED, TAG_NAMES.FRAME, TAG_NAMES.HR, TAG_NAMES.IMG, TAG_NAMES.INPUT, TAG_NAMES.KEYGEN, TAG_NAMES.LINK, TAG_NAMES.META, TAG_NAMES.PARAM, TAG_NAMES.SOURCE, TAG_NAMES.TRACK, TAG_NAMES.WBR;
7024
+ //#endregion
7025
+ //#region node_modules/.pnpm/entities@8.1.0/node_modules/entities/dist/escape.js
7026
+ /**
7027
+ * Get the named reference for an XML special character or U+00A0.
7028
+ * @param char Code unit matched by one of the escape regexes.
7029
+ */
7030
+ function getEscape(char) {
7031
+ return char === 34 ? """ : char === 38 ? "&" : char === 39 ? "'" : char === 60 ? "<" : char === 62 ? ">" : " ";
7032
+ }
7033
+ /**
7034
+ * Escape `data` using `re`, mapping each matched character to its entity.
7035
+ * Every match is one UTF-16 code unit, so its index is `lastIndex - 1`.
7036
+ * @param re Global regex matching exactly the characters to escape
7037
+ * (`"`, `&`, `'`, `<`, `>`, `\u00A0` at most).
7038
+ * @param data String to escape.
7039
+ */
7040
+ function escapeWithRegex(re, data) {
7041
+ re.lastIndex = 0;
7042
+ if (!re.test(data)) return data;
7043
+ let out = "";
7044
+ let last = 0;
7045
+ do {
7046
+ const index = re.lastIndex - 1;
7047
+ if (last !== index) out += data.substring(last, index);
7048
+ const char = data.charCodeAt(index);
7049
+ out += getEscape(char);
7050
+ last = index + 1;
7051
+ } while (re.test(data));
7052
+ return out + data.substring(last);
7053
+ }
7054
+ const attributeEscapeRegex = /["&\u{A0}]/gu;
7055
+ /**
7056
+ * Encodes all characters that have to be escaped in HTML attributes,
7057
+ * following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
7058
+ * @param data String to escape.
7059
+ */
7060
+ function escapeAttribute(data) {
7061
+ return escapeWithRegex(attributeEscapeRegex, data);
7062
+ }
7063
+ const textEscapeRegex = /[&<>\u{A0}]/gu;
7064
+ /**
7065
+ * Encodes all characters that have to be escaped in HTML text,
7066
+ * following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
7067
+ * @param data String to escape.
7068
+ */
7069
+ function escapeText(data) {
7070
+ return escapeWithRegex(textEscapeRegex, data);
7071
+ }
7072
+ //#endregion
7073
+ //#region node_modules/.pnpm/parse5@8.0.1/node_modules/parse5/dist/serializer/index.js
7074
+ const VOID_ELEMENTS = /* @__PURE__ */ new Set([
7075
+ TAG_NAMES.AREA,
7076
+ TAG_NAMES.BASE,
7077
+ TAG_NAMES.BASEFONT,
7078
+ TAG_NAMES.BGSOUND,
7079
+ TAG_NAMES.BR,
7080
+ TAG_NAMES.COL,
7081
+ TAG_NAMES.EMBED,
7082
+ TAG_NAMES.FRAME,
7083
+ TAG_NAMES.HR,
7084
+ TAG_NAMES.IMG,
7085
+ TAG_NAMES.INPUT,
7086
+ TAG_NAMES.KEYGEN,
7087
+ TAG_NAMES.LINK,
7088
+ TAG_NAMES.META,
7089
+ TAG_NAMES.PARAM,
7090
+ TAG_NAMES.SOURCE,
7091
+ TAG_NAMES.TRACK,
7092
+ TAG_NAMES.WBR
7093
+ ]);
7094
+ function isVoidElement(node, options) {
7095
+ return options.treeAdapter.isElementNode(node) && options.treeAdapter.getNamespaceURI(node) === NS.HTML && VOID_ELEMENTS.has(options.treeAdapter.getTagName(node));
7096
+ }
7097
+ const defaultOpts = {
7098
+ treeAdapter: defaultTreeAdapter,
7099
+ scriptingEnabled: true
7100
+ };
7101
+ /**
7102
+ * Serializes an AST node to an HTML string.
7103
+ *
7104
+ * @example
7105
+ *
7106
+ * ```js
7107
+ * const parse5 = require('parse5');
7108
+ *
7109
+ * const document = parse5.parse('<!DOCTYPE html><html><head></head><body>Hi there!</body></html>');
7110
+ *
7111
+ * // Serializes a document.
7112
+ * const html = parse5.serialize(document);
7113
+ *
7114
+ * // Serializes the <html> element content.
7115
+ * const str = parse5.serialize(document.childNodes[1]);
7116
+ *
7117
+ * console.log(str); //> '<head></head><body>Hi there!</body>'
7118
+ * ```
7119
+ *
7120
+ * @param node Node to serialize.
7121
+ * @param options Serialization options.
7122
+ */
7123
+ function serialize(node, options) {
7124
+ const opts = {
7125
+ ...defaultOpts,
7126
+ ...options
7127
+ };
7128
+ if (isVoidElement(node, opts)) return "";
7129
+ return serializeChildNodes(node, opts);
7130
+ }
7131
+ /**
7132
+ * Serializes an AST element node to an HTML string, including the element node.
7133
+ *
7134
+ * @example
7135
+ *
7136
+ * ```js
7137
+ * const parse5 = require('parse5');
7138
+ *
7139
+ * const document = parse5.parseFragment('<div>Hello, <b>world</b>!</div>');
7140
+ *
7141
+ * // Serializes the <div> element.
7142
+ * const str = parse5.serializeOuter(document.childNodes[0]);
7143
+ *
7144
+ * console.log(str); //> '<div>Hello, <b>world</b>!</div>'
7145
+ * ```
7146
+ *
7147
+ * @param node Node to serialize.
7148
+ * @param options Serialization options.
7149
+ */
7150
+ function serializeOuter(node, options) {
7151
+ return serializeNode(node, {
7152
+ ...defaultOpts,
7153
+ ...options
7154
+ });
7155
+ }
7156
+ function serializeChildNodes(parentNode, options) {
7157
+ let html = "";
7158
+ const container = options.treeAdapter.isElementNode(parentNode) && options.treeAdapter.getTagName(parentNode) === TAG_NAMES.TEMPLATE && options.treeAdapter.getNamespaceURI(parentNode) === NS.HTML ? options.treeAdapter.getTemplateContent(parentNode) : parentNode;
7159
+ const childNodes = options.treeAdapter.getChildNodes(container);
7160
+ if (childNodes) for (const currentNode of childNodes) html += serializeNode(currentNode, options);
7161
+ return html;
7162
+ }
7163
+ function serializeNode(node, options) {
7164
+ if (options.treeAdapter.isElementNode(node)) return serializeElement(node, options);
7165
+ if (options.treeAdapter.isTextNode(node)) return serializeTextNode(node, options);
7166
+ if (options.treeAdapter.isCommentNode(node)) return serializeCommentNode(node, options);
7167
+ if (options.treeAdapter.isDocumentTypeNode(node)) return serializeDocumentTypeNode(node, options);
7168
+ return "";
7169
+ }
7170
+ function serializeElement(node, options) {
7171
+ const tn = options.treeAdapter.getTagName(node);
7172
+ return `<${tn}${serializeAttributes(node, options)}>${isVoidElement(node, options) ? "" : `${serializeChildNodes(node, options)}</${tn}>`}`;
7173
+ }
7174
+ function serializeAttributes(node, { treeAdapter }) {
7175
+ let html = "";
7176
+ for (const attr of treeAdapter.getAttrList(node)) {
7177
+ html += " ";
7178
+ if (attr.namespace) switch (attr.namespace) {
7179
+ case NS.XML:
7180
+ html += `xml:${attr.name}`;
7181
+ break;
7182
+ case NS.XMLNS:
7183
+ if (attr.name !== "xmlns") html += "xmlns:";
7184
+ html += attr.name;
7185
+ break;
7186
+ case NS.XLINK:
7187
+ html += `xlink:${attr.name}`;
7188
+ break;
7189
+ default: html += `${attr.prefix}:${attr.name}`;
7190
+ }
7191
+ else html += attr.name;
7192
+ html += `="${escapeAttribute(attr.value)}"`;
7193
+ }
7194
+ return html;
7195
+ }
7196
+ function serializeTextNode(node, options) {
7197
+ const { treeAdapter } = options;
7198
+ const content = treeAdapter.getTextNodeContent(node);
7199
+ const parent = treeAdapter.getParentNode(node);
7200
+ const parentTn = parent && treeAdapter.isElementNode(parent) && treeAdapter.getTagName(parent);
7201
+ return parentTn && treeAdapter.getNamespaceURI(parent) === NS.HTML && hasUnescapedText(parentTn, options.scriptingEnabled) ? content : escapeText(content);
7202
+ }
7203
+ function serializeCommentNode(node, { treeAdapter }) {
7204
+ return `<!--${treeAdapter.getCommentNodeContent(node)}-->`;
7205
+ }
7206
+ function serializeDocumentTypeNode(node, { treeAdapter }) {
7207
+ return `<!DOCTYPE ${treeAdapter.getDocumentTypeNodeName(node)}>`;
7208
+ }
7014
7209
  //#endregion
7015
7210
  //#region node_modules/.pnpm/parse5@8.0.1/node_modules/parse5/dist/index.js
7016
7211
  /**
@@ -7044,4 +7239,4 @@ function parseFragment(fragmentContext, html, options) {
7044
7239
  return parser.getFragment();
7045
7240
  }
7046
7241
  //#endregion
7047
- export { parseFragment as n, parse as t };
7242
+ export { serializeOuter as i, parseFragment as n, serialize as r, parse as t };
@@ -308,7 +308,7 @@ function rewriteEmbeds(html, context) {
308
308
  }
309
309
  const tag = (pairedTag ?? voidTag).toLowerCase();
310
310
  const attrs = attributesOf((pairedAttrs ?? voidAttrs) || "");
311
- const reference = tag === "object" ? attrs.data : attrs.src;
311
+ const reference = tag === "object" ? attrs.data : attrs.src ?? attrs["data-src"];
312
312
  if (reference === void 0) {
313
313
  if (tag === "iframe" && attrs.srcdoc === void 0) return whole;
314
314
  return block(tag, "inline", null, "unknown-host");
@@ -333,7 +333,7 @@ function rewriteEmbeds(html, context) {
333
333
  ...width !== void 0 ? { width } : {},
334
334
  ...height !== void 0 ? { height } : {},
335
335
  attributes: rest,
336
- decorative: /\bwp-block-getquick-design-video-hero__video\b/.test(attrs.class ?? "")
336
+ decorative: /\b(?:wp-block-getquick-design-video-hero__video|gqd-video-hero__video)\b/.test(attrs.class ?? "")
337
337
  });
338
338
  }),
339
339
  placeholders,
@@ -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 };
@@ -0,0 +1,193 @@
1
+ import { a as blockFormId, o as findForm, r as GRAVITY_FORMS_BLOCK, s as formAction } from "./descriptor-x8kHFQ79.js";
2
+ import { a as clearFormStateCookie, i as FORM_STATE_PARAM, l as readFormState, n as formsCopyLanguage, t as FORMS_COPY } from "./copy-DLto_bWj.js";
3
+ //#region frontend/src/forms/astro.ts
4
+ function escapeHtml(value) {
5
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
6
+ }
7
+ /** Text without tags: Gravity Forms allows HTML in a consent's checkbox label. */
8
+ const tagless = (value) => value.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
9
+ const attribute = (name, value) => value === null || value === void 0 ? "" : ` ${name}="${escapeHtml(value)}"`;
10
+ /** The HTML id of an input (`gq-form-1-input_1_3`). */
11
+ const inputId = (formId, name) => `gq-form-${formId}-${name}`;
12
+ const AUTOCOMPLETE = {
13
+ email: "email",
14
+ phone: "tel",
15
+ website: "url"
16
+ };
17
+ const INPUT_TYPE = {
18
+ text: "text",
19
+ email: "email",
20
+ phone: "tel",
21
+ website: "url",
22
+ number: "text"
23
+ };
24
+ function errorText(field, context) {
25
+ const message = context.state?.messages[field.id];
26
+ if (message) return message;
27
+ const code = context.state?.errors[field.id];
28
+ return code ? context.copy.errors[code] : null;
29
+ }
30
+ /** The value to show: what the visitor typed, after a failed post, else the field's default. */
31
+ /** What the visitor typed, when the outcome kept it; otherwise the form shows its defaults. */
32
+ function typed(context) {
33
+ const state = context.state;
34
+ return state && state.status !== "sent" && Object.keys(state.values).length ? state.values : null;
35
+ }
36
+ function shown(name, field, context) {
37
+ const values = typed(context);
38
+ if (values) return values[name] ?? "";
39
+ return field.defaultValue ?? "";
40
+ }
41
+ function requiredMark(field, copy) {
42
+ return field.required ? ` <span class="gq-form__required" aria-hidden="true">*</span><span class="gq-form__visually-hidden"> (${escapeHtml(copy.required)})</span>` : "";
43
+ }
44
+ /** The description and error elements of a field, and the ids that describe its input. */
45
+ function annotations(field, context) {
46
+ const base = `gq-form-${context.formId}-field-${field.id}`;
47
+ const error = errorText(field, context);
48
+ const ids = [];
49
+ let html = "";
50
+ if (field.description) {
51
+ ids.push(`${base}-description`);
52
+ html += `<p class="gq-form__description" id="${base}-description">${escapeHtml(field.description)}</p>`;
53
+ }
54
+ if (error) {
55
+ ids.push(`${base}-error`);
56
+ html += `<p class="gq-form__error" id="${base}-error">${escapeHtml(error)}</p>`;
57
+ }
58
+ return {
59
+ html,
60
+ describedBy: ids.length ? ids.join(" ") : null,
61
+ invalid: Boolean(error),
62
+ base
63
+ };
64
+ }
65
+ function inputAttributes(field, context, notes) {
66
+ return attribute("aria-describedby", notes.describedBy) + (notes.invalid ? " aria-invalid=\"true\"" : "") + (field.required ? " required aria-required=\"true\"" : "");
67
+ }
68
+ function fieldClass(field, invalid, extra = "") {
69
+ return `gq-form__field gq-form__field--${field.type}${extra}` + (invalid ? " gq-form__field--invalid" : "") + (field.cssClass ? ` ${field.cssClass}` : "");
70
+ }
71
+ function renderField(field, context) {
72
+ const { formId, copy } = context;
73
+ if (field.type === "html") return `<div class="gq-form__html${field.cssClass ? ` ${escapeHtml(field.cssClass)}` : ""}">${field.content ?? ""}</div>`;
74
+ if (field.type === "section") return `<div class="gq-form__section"><h3 class="gq-form__section-title">${escapeHtml(field.label)}</h3>` + (field.description ? `<p class="gq-form__description">${escapeHtml(field.description)}</p>` : "") + `</div>`;
75
+ if (field.type === "hidden" || field.visibility === "hidden") return field.inputs.map((input) => `<input type="hidden" name="${escapeHtml(input.name)}" value="${escapeHtml(field.defaultValue ?? "")}">`).join("");
76
+ const notes = annotations(field, context);
77
+ const first = field.inputs[0];
78
+ if (!first) return "";
79
+ const classes = escapeHtml(fieldClass(field, notes.invalid));
80
+ const label = escapeHtml(field.label) + requiredMark(field, copy);
81
+ if (field.type === "radio" || field.type === "checkbox") {
82
+ const options = (field.choices ?? []).map((choice, index) => {
83
+ const input = field.type === "checkbox" ? field.inputs[index] : first;
84
+ if (!input) return "";
85
+ const id = `${inputId(formId, input.name)}-${index}`;
86
+ const values = typed(context);
87
+ const checked = values ? values[input.name] === choice.value : choice.selected;
88
+ return `<div class="gq-form__choice"><input type="${field.type}" id="${id}" name="${escapeHtml(input.name)}" value="${escapeHtml(choice.value)}"${checked ? " checked" : ""}${attribute("aria-describedby", notes.describedBy)}${notes.invalid ? " aria-invalid=\"true\"" : ""}><label for="${id}">${escapeHtml(choice.text)}</label></div>`;
89
+ });
90
+ return `<fieldset class="${classes}"${field.required ? " aria-required=\"true\"" : ""}><legend class="gq-form__label">${label}</legend>` + notes.html + options.join("") + `</fieldset>`;
91
+ }
92
+ if (field.type === "consent") {
93
+ const id = inputId(formId, first.name);
94
+ const checked = shown(first.name, field, context) === "1";
95
+ const policy = context.privacyPolicy ? ` <a class="gq-form__policy" href="${escapeHtml(context.privacyPolicy.url)}">${escapeHtml(context.privacyPolicy.title)}</a>` : "";
96
+ const consentText = field.consent?.description ? `<p class="gq-form__description gq-form__consent-text" id="${notes.base}-text">${escapeHtml(field.consent.description)}</p>` : "";
97
+ const describedBy = [notes.describedBy, consentText ? `${notes.base}-text` : null].filter(Boolean).join(" ");
98
+ return `<fieldset class="${classes}"><legend class="gq-form__label">${label}</legend>` + notes.html + consentText + `<div class="gq-form__choice"><input type="checkbox" id="${id}" name="${escapeHtml(first.name)}" value="1"${checked ? " checked" : ""}${attribute("aria-describedby", describedBy || null)}${notes.invalid ? " aria-invalid=\"true\"" : ""}${field.required ? " required aria-required=\"true\"" : ""}><label for="${id}">${escapeHtml(tagless(field.consent?.checkboxLabel ?? "") || field.label)}</label>${policy}</div></fieldset>`;
99
+ }
100
+ if (field.type === "name" && field.inputs.length > 1) {
101
+ const parts = field.inputs.map((input) => {
102
+ const id = inputId(formId, input.name);
103
+ return `<div class="gq-form__name-part"><label for="${id}">${escapeHtml(input.label)}</label><input type="text" id="${id}" name="${escapeHtml(input.name)}" value="${escapeHtml(shown(input.name, field, context))}"${attribute("autocomplete", input.autocomplete)}${attribute("placeholder", input.placeholder)}${attribute("aria-describedby", notes.describedBy)}${notes.invalid ? " aria-invalid=\"true\"" : ""}></div>`;
104
+ });
105
+ return `<fieldset class="${classes}"${field.required ? " aria-required=\"true\"" : ""}><legend class="gq-form__label">${label}</legend>` + notes.html + parts.join("") + `</fieldset>`;
106
+ }
107
+ const id = inputId(formId, first.name);
108
+ const common = attribute("placeholder", field.placeholder) + (field.maxLength ? ` maxlength="${field.maxLength}"` : "") + inputAttributes(field, context, notes);
109
+ let control;
110
+ if (field.type === "textarea") control = `<textarea id="${id}" name="${escapeHtml(first.name)}" rows="6"${common}>${escapeHtml(shown(first.name, field, context))}</textarea>`;
111
+ else if (field.type === "select") {
112
+ const value = typed(context) ? shown(first.name, field, context) : null;
113
+ const choices = field.choices ?? [];
114
+ const options = (choices.some((choice) => choice.value === "") ? "" : `<option value=""${value === "" ? " selected" : ""}></option>`) + choices.map((choice) => {
115
+ const selected = value !== null ? value === choice.value : choice.selected;
116
+ return `<option value="${escapeHtml(choice.value)}"${selected ? " selected" : ""}>${escapeHtml(choice.text)}</option>`;
117
+ }).join("");
118
+ control = `<select id="${id}" name="${escapeHtml(first.name)}"${inputAttributes(field, context, notes)}>${options}</select>`;
119
+ } else {
120
+ const inputmode = field.type === "number" ? " inputmode=\"decimal\"" : "";
121
+ const autocomplete = field.type === "name" ? "name" : AUTOCOMPLETE[field.type] ?? first.autocomplete;
122
+ control = `<input type="${INPUT_TYPE[field.type] ?? "text"}" id="${id}" name="${escapeHtml(first.name)}" value="${escapeHtml(shown(first.name, field, context))}"${attribute("autocomplete", autocomplete)}${inputmode}${common}>`;
123
+ }
124
+ let confirmation = "";
125
+ const second = field.inputs[1];
126
+ if (field.type === "email" && second) {
127
+ const confirmId = inputId(formId, second.name);
128
+ confirmation = `<label class="gq-form__label" for="${confirmId}">${escapeHtml(second.label)}${requiredMark(field, copy)}</label><input type="email" id="${confirmId}" name="${escapeHtml(second.name)}" value="${escapeHtml(shown(second.name, field, context))}" autocomplete="email"${inputAttributes(field, context, notes)}>`;
129
+ }
130
+ return `<div class="${classes}"><label class="gq-form__label" for="${id}">${label}</label>` + notes.html + control + confirmation + `</div>`;
131
+ }
132
+ /** Turnstile loads only once the visitor interacts with a form (CONSENT-1, CN-R3). */
133
+ 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})}})();`;
134
+ /**
135
+ * A `gravityforms/form` block's HTML. Without a descriptor (the CMS hasn't
136
+ * delivered one) or for an unavailable form, a status message instead of a
137
+ * form that can't be sent.
138
+ */
139
+ function renderGravityForm(input) {
140
+ const copy = FORMS_COPY[formsCopyLanguage(input.language)];
141
+ const descriptor = input.descriptor;
142
+ if (!descriptor || !descriptor.available) return `<div class="gq-form gq-form--unavailable"${descriptor ? ` id="gq-form-${descriptor.id}"` : ""}><p class="gq-form__status" role="status">${escapeHtml(copy.unavailable)}</p></div>`;
143
+ const formId = descriptor.id;
144
+ const state = input.state && input.state.formId === formId ? input.state : null;
145
+ const policy = input.legal?.roles.find((role) => role.role === "privacy_policy");
146
+ const context = {
147
+ formId,
148
+ copy,
149
+ state,
150
+ privacyPolicy: policy ? {
151
+ url: policy.url,
152
+ title: policy.title || copy.privacyPolicy
153
+ } : null
154
+ };
155
+ const level = input.headingLevel ?? 2;
156
+ const titleId = `gq-form-${formId}-title`;
157
+ const heading = input.showTitle !== false && descriptor.title ? `<h${level} class="gq-form__title" id="${titleId}">${escapeHtml(descriptor.title)}</h${level}>` : "";
158
+ const description = input.showDescription !== false && descriptor.description ? `<p class="gq-form__intro">${escapeHtml(descriptor.description)}</p>` : "";
159
+ const open = `<div class="gq-form" id="gq-form-${formId}" data-gq-form="${formId}" lang="${escapeHtml(input.language)}">${heading}${description}`;
160
+ if (state?.status === "sent") return `${open}<div class="gq-form__status gq-form__status--sent" role="status">${(state.confirmation || (descriptor.confirmation.type === "message" ? descriptor.confirmation.message : "") || copy.sent).split(/\n{2,}/).map((paragraph) => `<p>${escapeHtml(paragraph.trim())}</p>`).join("")}</div></div>`;
161
+ let status = "";
162
+ if (state) {
163
+ const problems = descriptor.fields.flatMap((field) => {
164
+ const error = errorText(field, context);
165
+ if (!error) return [];
166
+ const target = field.inputs[0];
167
+ return [`<li><a href="#${!target ? `gq-form-${formId}` : field.type === "radio" || field.type === "checkbox" ? `${inputId(formId, target.name)}-0` : inputId(formId, target.name)}">${escapeHtml(field.label)}: ${escapeHtml(error)}</a></li>`];
168
+ });
169
+ status = `<div class="gq-form__errors" role="alert" id="gq-form-${formId}-errors" tabindex="-1"><p>${escapeHtml(state.status === "invalid" ? copy.errorSummary : state.status === "verification" ? copy.verification : state.status === "too-many" ? copy.tooMany : copy.failed)}</p>${problems.length ? `<ul>${problems.join("")}</ul>` : ""}</div>`;
170
+ }
171
+ const required = descriptor.fields.some((field) => field.required && field.visibility === "visible") ? `<p class="gq-form__legend">${escapeHtml(copy.requiredFields)}</p>` : "";
172
+ const honeypot = descriptor.honeypot ? `<div class="gq-form__hp" aria-hidden="true" style="position:absolute!important;left:-9999px!important;width:1px;height:1px;overflow:hidden"><label for="${inputId(formId, descriptor.honeypot.name)}">${escapeHtml(copy.honeypot)}</label><input type="text" id="${inputId(formId, descriptor.honeypot.name)}" name="${escapeHtml(descriptor.honeypot.name)}" value="" tabindex="-1" autocomplete="off"></div>` : "";
173
+ const turnstile = input.turnstile?.siteKey ? `<div class="gq-form__turnstile" data-gq-turnstile-widget></div><noscript><p class="gq-form__status">${escapeHtml(copy.needsJavaScript)}</p></noscript>` : "";
174
+ const script = input.turnstile?.siteKey ? `<script>${FORMS_TURNSTILE_SCRIPT}<\/script>` : "";
175
+ return open + status + `<form class="gq-form__form" method="post" action="${formAction(formId)}" novalidate` + (heading ? ` aria-labelledby="${titleId}"` : "") + attribute("data-gq-turnstile", input.turnstile?.siteKey || null) + `><input type="hidden" name="gq_source" value="${escapeHtml(input.source)}">` + required + honeypot + descriptor.fields.map((field) => renderField(field, context)).join("") + turnstile + `<div class="gq-form__actions"><button type="submit" class="gq-form__submit">${escapeHtml(descriptor.submitLabel || copy.submit)}</button></div></form></div>` + script;
176
+ }
177
+ /**
178
+ * A minimal default stylesheet (layout and states only). Sites usually style
179
+ * the `gq-form*` classes with their own tokens instead.
180
+ */
181
+ const FORMS_STYLES = `
182
+ .gq-form__visually-hidden{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
183
+ .gq-form__form{display:grid;gap:var(--gq-form-gap,1rem)}
184
+ .gq-form__field{display:grid;gap:.375rem;border:0;margin:0;padding:0;min-width:0}
185
+ .gq-form__field input:not([type=checkbox]):not([type=radio]),.gq-form__field textarea,.gq-form__field select{font:inherit;width:100%;padding:.5rem .75rem;border:1px solid var(--gq-form-border,#767676);border-radius:var(--gq-form-radius,.25rem)}
186
+ .gq-form__field--invalid input,.gq-form__field--invalid textarea,.gq-form__field--invalid select{border-color:var(--gq-form-error,#b00020)}
187
+ .gq-form__error,.gq-form__errors{color:var(--gq-form-error,#b00020)}
188
+ .gq-form__errors{border:2px solid currentColor;padding:1rem}
189
+ .gq-form__choice{display:flex;gap:.5rem;align-items:baseline}
190
+ .gq-form :focus-visible{outline:3px solid var(--gq-form-focus,#1a73e8);outline-offset:2px}
191
+ `.trim();
192
+ //#endregion
193
+ export { FORMS_STYLES, FORMS_TURNSTILE_SCRIPT, FORM_STATE_PARAM, GRAVITY_FORMS_BLOCK, blockFormId, clearFormStateCookie, escapeHtml, findForm, inputId, readFormState, renderGravityForm };
@@ -0,0 +1,80 @@
1
+ import { o as FormDescriptor, t as FORMS_PATH } from "./descriptor-B-e7gHi1.js";
2
+ //#region frontend/src/forms/server.d.ts
3
+ /** The largest form body accepted (bytes); larger posts are refused unread. */
4
+ export declare const MAX_FORM_BYTES: number;
5
+ /** Cloudflare's server-side Turnstile verification. */
6
+ export declare const TURNSTILE_SITEVERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
7
+ /** The field Turnstile's widget posts its token in. */
8
+ export declare const TURNSTILE_RESPONSE_FIELD = "cf-turnstile-response";
9
+ /** A Workers Rate Limiting binding. */
10
+ export interface FormsRateLimiter {
11
+ limit(options: {
12
+ key: string;
13
+ }): Promise<{
14
+ success: boolean;
15
+ }>;
16
+ }
17
+ /** The Worker bindings the handler reads. None is a Gravity Forms credential. */
18
+ export interface FormsBindings {
19
+ /** Turnstile's secret key (a Worker secret), when the Site uses Turnstile. */
20
+ TURNSTILE_SECRET_KEY?: string;
21
+ /**
22
+ * A Cloudflare Access service token for the CMS's submissions path only,
23
+ * when the CMS is behind Access. Both or neither.
24
+ */
25
+ GQ_FORMS_ACCESS_CLIENT_ID?: string;
26
+ GQ_FORMS_ACCESS_CLIENT_SECRET?: string;
27
+ /** Rate limiting for form posts, keyed per client IP per request. */
28
+ FORMS_RATE_LIMITER?: FormsRateLimiter;
29
+ }
30
+ export interface FormLookup {
31
+ formId: number;
32
+ /** The page the form was posted from (a path on the Site). */
33
+ source: string;
34
+ }
35
+ export interface FormsServerConfig {
36
+ /** The CMS's origin (https://cms.example.com): Gravity Forms' REST API is under it. */
37
+ cmsOrigin: string;
38
+ /** WordPress's REST root, when not `<cmsOrigin>/wp-json`. */
39
+ restRoot?: string;
40
+ /**
41
+ * The form's descriptor as published with the page it was posted from
42
+ * (the content runtime's `publishedForm(source, formId)`), or null.
43
+ */
44
+ descriptor: (lookup: FormLookup) => Promise<FormDescriptor | null> | FormDescriptor | null;
45
+ /** The Site's spam controls. The honeypot is always applied when a form enables it. */
46
+ spam?: {
47
+ turnstile?: boolean;
48
+ };
49
+ bindings: () => FormsBindings | Promise<FormsBindings>;
50
+ /** The Site's public origin(s), when the Worker sees another (a proxy). */
51
+ siteOrigins?: readonly string[];
52
+ /** The page language for the handler's own messages (`pt-PT`, `en`). */
53
+ language?: string;
54
+ /** Milliseconds Gravity Forms and Turnstile have to answer (15 s). */
55
+ timeoutMs?: number;
56
+ fetch?: typeof globalThis.fetch;
57
+ }
58
+ /** HTML as text: tags dropped, paragraphs and breaks as new lines, entities decoded. */
59
+ export declare function htmlText(html: string): string;
60
+ /**
61
+ * The form endpoint. `handle` answers `POST /gq/forms/<id>` and null for any
62
+ * other path; `check` says what is configured, for readiness, never values.
63
+ */
64
+ export declare function createFormsServer(config: FormsServerConfig): {
65
+ handle: (request: Request) => Promise<Response | null>;
66
+ check: () => Promise<{
67
+ cmsOrigin: string;
68
+ spam: {
69
+ honeypot: boolean;
70
+ turnstile: boolean;
71
+ };
72
+ turnstileSecret: boolean;
73
+ access: string;
74
+ rateLimit: boolean;
75
+ ready: boolean;
76
+ }>;
77
+ };
78
+ export type FormsServer = ReturnType<typeof createFormsServer>;
79
+ //#endregion
80
+ export { FORMS_PATH, type FormDescriptor };