@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.
- package/README.md +87 -0
- package/dist/consent-astro.js +2 -2
- package/dist/consent-client.js +19 -6
- package/dist/consent-server.js +4 -37
- package/dist/consent.js +2 -2
- package/dist/copy-DLto_bWj.js +163 -0
- package/dist/descriptor-B-e7gHi1.d.ts +143 -0
- package/dist/descriptor-x8kHFQ79.js +205 -0
- package/dist/design-client.d.ts +19 -0
- package/dist/design-client.js +519 -0
- package/dist/{dist-D4-CMkrf.js → dist-5jNKLmbn.js} +198 -3
- package/dist/{embeds-p9SyGNZI.js → embeds-CJJemAmE.js} +2 -2
- package/dist/forms-astro.d.ts +50 -0
- package/dist/forms-astro.js +193 -0
- package/dist/forms-server.d.ts +80 -0
- package/dist/forms-server.js +294 -0
- package/dist/forms-testing.d.ts +66 -0
- package/dist/forms-testing.js +334 -0
- package/dist/forms.d.ts +25 -0
- package/dist/forms.js +4 -0
- package/dist/index.d.ts +131 -12
- package/dist/index.js +9 -2
- package/dist/metadata-content.js +1 -1
- package/dist/metadata-testing.js +1 -1
- package/dist/request-guards-BUPr_vA4.js +46 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +281 -26
- package/dist/site-language-BCGjkpWY.js +169178 -0
- package/dist/state-CsMFLUUO.d.ts +66 -0
- package/dist/validate-wRgYUNxN.js +130 -0
- package/dist/{version-ChwS3fUu.js → version-D81MsCZU.js} +1 -1
- package/dist/video-hero-D8AcMR4q.js +69 -0
- package/dist/{wordpress-D_HG2rEt.d.ts → wordpress-BvkTDMA0.d.ts} +306 -3
- package/package.json +21 -1
- package/dist/site-language-BLA2mmOw.js +0 -565
|
@@ -1574,7 +1574,18 @@ const NUMBERED_HEADERS = /* @__PURE__ */ new Set([
|
|
|
1574
1574
|
$.H5,
|
|
1575
1575
|
$.H6
|
|
1576
1576
|
]);
|
|
1577
|
-
|
|
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
|
-
|
|
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: /\
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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 };
|