@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.
@@ -0,0 +1,66 @@
1
+ //#region frontend/src/forms/copy.d.ts
2
+ type FormsCopyLanguage = "en" | "pt-PT";
3
+ type FormErrorCode = "required" | "email" | "email-mismatch" | "number" | "range" | "url" | "choice" | "too-long";
4
+ interface FormsCopy {
5
+ required: string;
6
+ requiredFields: string;
7
+ errorSummary: string;
8
+ errors: Record<FormErrorCode, string>;
9
+ /** The submission was refused before reaching the CMS, or the CMS failed. */
10
+ failed: string;
11
+ /** Turnstile failed or was missing. */
12
+ verification: string;
13
+ /** A submission arrived too fast or too often. */
14
+ tooMany: string;
15
+ sent: string;
16
+ unavailable: string;
17
+ submit: string;
18
+ privacyPolicy: string;
19
+ /** Visible to assistive technology only, on the honeypot (Gravity Forms' wording). */
20
+ honeypot: string;
21
+ /** With Turnstile and no JavaScript. */
22
+ needsJavaScript: string;
23
+ }
24
+ declare const FORMS_COPY: Record<FormsCopyLanguage, FormsCopy>;
25
+ /** The copy language for a page's `lang`: Portuguese for `pt*`, else English. */
26
+ declare function formsCopyLanguage(language: string | null | undefined): FormsCopyLanguage;
27
+ //#endregion
28
+ //#region frontend/src/forms/state.d.ts
29
+ type FormStatus = "sent" | "invalid" | "failed" | "verification" | "too-many";
30
+ interface FormState {
31
+ formId: number;
32
+ status: FormStatus;
33
+ /** Field errors by field id: the descriptor check's codes. */
34
+ errors: Record<number, FormErrorCode>;
35
+ /** Gravity Forms' own messages by field id, when it refused the submission. */
36
+ messages: Record<number, string>;
37
+ /** What the visitor typed, by input name (left out when too large to keep). */
38
+ values: Record<string, string>;
39
+ /** Gravity Forms' confirmation message as text, after a submission it accepted. */
40
+ confirmation?: string;
41
+ }
42
+ /** The query parameter that marks the redirect back after a post. */
43
+ declare const FORM_STATE_PARAM = "gq-form";
44
+ /** Seconds the outcome is kept for the page after the redirect. */
45
+ declare const FORM_STATE_MAX_AGE = 300;
46
+ declare const formStateCookieName: (formId: number) => string;
47
+ /** The Set-Cookie header that carries a form's outcome to the page. */
48
+ declare function formStateCookie(state: FormState, options: {
49
+ secure: boolean;
50
+ }): string;
51
+ /** The Set-Cookie header that removes a form's outcome once the page has shown it. */
52
+ declare function clearFormStateCookie(formId: number, options: {
53
+ secure: boolean;
54
+ }): string;
55
+ /**
56
+ * A form's outcome for the page: present only on the redirect back after a
57
+ * post to it (`?gq-form=<id>`), from its cookie. Null otherwise.
58
+ */
59
+ declare function readFormState(request: {
60
+ url: string | URL;
61
+ cookie: string | null | undefined;
62
+ }, formId: number): FormState | null;
63
+ /** Where the handler sends the visitor back to: the page, marked for the form. */
64
+ declare function formStateLocation(source: string, formId: number): string;
65
+ //#endregion
66
+ export { clearFormStateCookie as a, formStateLocation as c, FormErrorCode as d, FormsCopy as f, FormStatus as i, readFormState as l, formsCopyLanguage as m, FORM_STATE_PARAM as n, formStateCookie as o, FormsCopyLanguage as p, FormState as r, formStateCookieName as s, FORM_STATE_MAX_AGE as t, FORMS_COPY as u };
@@ -0,0 +1,130 @@
1
+ //#region frontend/src/forms/validate.ts
2
+ /** No single value is longer (characters). */
3
+ const MAX_VALUE_LENGTH = 1e4;
4
+ const EMAIL = /^[^\s@<>()[\]\\,;:"]+@[^\s@<>()[\]\\,;:"]+\.[^\s@<>()[\]\\,;:"]+$/;
5
+ function webAddress(value) {
6
+ try {
7
+ const url = new URL(value);
8
+ return (url.protocol === "https:" || url.protocol === "http:") && url.hostname.includes(".");
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * A number as visitors type it in either convention (1234.5, 1,234.5, 1.234,5,
15
+ * 1 234,5): the last separator is the decimal one when both appear. NaN when
16
+ * it isn't one; Gravity Forms checks its own number format again.
17
+ */
18
+ function parseNumber(value) {
19
+ let text = value.replace(/[\s\u00a0']/g, "");
20
+ const dot = text.lastIndexOf(".");
21
+ const comma = text.lastIndexOf(",");
22
+ if (dot >= 0 && comma >= 0) text = dot > comma ? text.replaceAll(",", "") : text.replaceAll(".", "").replace(",", ".");
23
+ else if (comma >= 0) text = text.indexOf(",") === comma ? text.replace(",", ".") : text.replaceAll(",", "");
24
+ else if ((text.match(/\./g) ?? []).length > 1) text = text.replaceAll(".", "");
25
+ return /^-?\d+(\.\d+)?$/.test(text) ? Number(text) : NaN;
26
+ }
27
+ /** A posted value, without surrounding whitespace, bounded. */
28
+ function read(values, name) {
29
+ return (values.get(name) ?? "").trim().slice(0, MAX_VALUE_LENGTH);
30
+ }
31
+ function check(field, values, input, shown) {
32
+ const take = (name) => {
33
+ const value = read(values, name);
34
+ input[name] = value;
35
+ if (field.visibility === "visible" && field.type !== "hidden") shown[name] = value;
36
+ return value;
37
+ };
38
+ const first = field.inputs[0];
39
+ switch (field.type) {
40
+ case "html":
41
+ case "section": return null;
42
+ case "hidden": {
43
+ if (!first) return null;
44
+ const value = values.get(first.name) === null ? field.defaultValue ?? "" : take(first.name);
45
+ input[first.name] = value;
46
+ return null;
47
+ }
48
+ case "checkbox": {
49
+ const chosen = field.inputs.filter((each) => {
50
+ const value = read(values, each.name);
51
+ if (value === "") return false;
52
+ input[each.name] = value;
53
+ shown[each.name] = value;
54
+ return true;
55
+ });
56
+ if (chosen.some((each) => read(values, each.name) !== each.value)) return "choice";
57
+ return field.required && chosen.length === 0 ? "required" : null;
58
+ }
59
+ case "consent": {
60
+ if (!first) return null;
61
+ const checked = read(values, first.name) === "1";
62
+ if (field.required && !checked) return "required";
63
+ if (checked) {
64
+ input[first.name] = "1";
65
+ shown[first.name] = "1";
66
+ const [, label, revision] = field.inputs;
67
+ if (label) input[label.name] = field.consent?.checkboxLabel ?? "";
68
+ if (revision) input[revision.name] = field.consent?.revisionId ?? "";
69
+ }
70
+ return null;
71
+ }
72
+ case "name": {
73
+ const given = field.inputs.map((each) => ({
74
+ each,
75
+ value: take(each.name)
76
+ }));
77
+ if (!field.required) return null;
78
+ const essential = given.filter(({ each }) => given.length === 1 || /\.(3|6)$/.test(each.id));
79
+ return (essential.length ? essential : given).some(({ value }) => value === "") ? "required" : null;
80
+ }
81
+ default: {
82
+ if (!first) return null;
83
+ const value = take(first.name);
84
+ if (value === "") {
85
+ if (field.type === "email" && field.inputs[1]) take(field.inputs[1].name);
86
+ return field.required ? "required" : null;
87
+ }
88
+ if (field.maxLength !== void 0 && Array.from(value).length > field.maxLength) return "too-long";
89
+ if (field.type === "email") {
90
+ if (!EMAIL.test(value)) return "email";
91
+ const confirmation = field.inputs[1];
92
+ if (confirmation && take(confirmation.name) !== value) return "email-mismatch";
93
+ }
94
+ if (field.type === "website" && !webAddress(value)) return "url";
95
+ if (field.type === "number") {
96
+ const number = parseNumber(value);
97
+ if (!Number.isFinite(number)) return "number";
98
+ if (field.range?.min !== void 0 && number < field.range.min || field.range?.max !== void 0 && number > field.range.max) return "range";
99
+ }
100
+ if ((field.type === "select" || field.type === "radio") && !(field.choices ?? []).some((choice) => choice.value === value)) return "choice";
101
+ return null;
102
+ }
103
+ }
104
+ }
105
+ /**
106
+ * Checks a submission against the descriptor: required fields, formats,
107
+ * choices and lengths. Only the descriptor's inputs are kept; the honeypot is
108
+ * passed through, empty, for Gravity Forms' own check.
109
+ */
110
+ function validateSubmission(descriptor, values) {
111
+ const input = {};
112
+ const shown = {};
113
+ const errors = {};
114
+ for (const field of descriptor.fields) {
115
+ const error = check(field, values, input, shown);
116
+ if (error) errors[field.id] = error;
117
+ }
118
+ if (descriptor.honeypot) input[descriptor.honeypot.name] = "";
119
+ return {
120
+ input,
121
+ errors,
122
+ values: shown
123
+ };
124
+ }
125
+ /** Whether the honeypot was filled in: a bot. Such a submission never reaches the CMS. */
126
+ function honeypotFilled(descriptor, values) {
127
+ return descriptor.honeypot !== null && (values.get(descriptor.honeypot.name) ?? "") !== "";
128
+ }
129
+ //#endregion
130
+ export { honeypotFilled as n, validateSubmission as r, MAX_VALUE_LENGTH as t };
@@ -1,6 +1,6 @@
1
1
  //#endregion
2
2
  //#region frontend/src/consent/version.ts
3
3
  /** The UI/adapter version recorded with each decision (CN-R7). */
4
- const CONSENT_UI_VERSION = `gq-content@0.3.2`;
4
+ const CONSENT_UI_VERSION = `gq-content@0.4.0`;
5
5
  //#endregion
6
6
  export { CONSENT_UI_VERSION as t };
@@ -1,5 +1,6 @@
1
1
  import { r as ConsentConfiguration, t as AnalyticsSettings } from "./configuration-C_JGygQI.js";
2
2
  import { r as LegalLanguage } from "./legal-C5Xiq59W.js";
3
+ import { o as FormDescriptor } from "./descriptor-B-e7gHi1.js";
3
4
  //#endregion
4
5
  //#region frontend/src/design-text.d.ts
5
6
  /** Already loaded, authorized values. No lookup, fetching or query selection.
@@ -118,7 +119,7 @@ type DesignThemeOverrides = { [K in keyof DesignTheme]?: DesignTheme[K] extends
118
119
  declare function resolveDesignTheme(overrides?: unknown): DesignTheme;
119
120
  /** Complete theme declarations for every supported value, independent of CMS content scanning. */
120
121
  declare function designThemeVariables(overrides?: DesignThemeOverrides): Record<string, string>;
121
- /** Exact pinned Preflight, deliberately scoped away from inspectors and wp-admin. */
122
+ /** Scoped Preflight; the Gutenberg canvas retains native and authored spacing. */
122
123
  declare function designThemeStyleSheet(overrides?: DesignThemeOverrides, scope?: string): string;
123
124
  /** Color/font extension for the shared presentation parser; never raw utility strings. */
124
125
  declare function resolvePresentationToken(kind: string, value: unknown): string | undefined;
@@ -140,6 +141,15 @@ interface PresentationModel {
140
141
  default?: string;
141
142
  }>>;
142
143
  defaults: Record<string, Record<string, string | number>>;
144
+ /**
145
+ * Frontend flow rhythm: listed blocks space themselves from a preceding
146
+ * sibling; layout blocks only from a preceding flow block in body content.
147
+ */
148
+ flow?: {
149
+ blocks: string[];
150
+ layout?: string[];
151
+ gap: string;
152
+ };
143
153
  }
144
154
  interface PresentationTargets {
145
155
  selector?: string;
@@ -215,6 +225,7 @@ declare function presentationVariables(name: string, presentation?: unknown, opt
215
225
  /** Static, complete CSS: content first authored after a production build still works. */
216
226
  declare function presentationStyleSheet(theme?: PresentationTheme, options?: {
217
227
  model?: PresentationModel;
228
+ editor?: boolean;
218
229
  }): string;
219
230
  declare namespace content_types_d_exports {
220
231
  export { Archives, CmsFailure, CmsFailureReason, CollectionMembership, CollectionTerm, Delivery, DesignPresets, EntryContent, Extended, Found, HomeContent, Missing, PublishedEntry, SiteChrome, Translation, Unavailable, WordPressImage, WordPressMenuItem, WordPressPost, WordPressSettings };
@@ -226,6 +237,11 @@ interface WordPressImage {
226
237
  interface WordPressPost {
227
238
  databaseId?: number;
228
239
  designBlocks?: DesignBlock[];
240
+ /**
241
+ * The public descriptors of the Gravity Forms forms it references, on a
242
+ * Site with `forms` (ADR 0009): what `gravityforms/form` blocks render.
243
+ */
244
+ forms?: FormDescriptor[];
229
245
  id: string;
230
246
  title: string;
231
247
  excerpt: string;
@@ -276,6 +292,8 @@ interface HomeContent<X = unknown> extends DesignPresets {
276
292
  hasVideoHero: boolean;
277
293
  databaseId?: number;
278
294
  designBlocks?: DesignBlock[];
295
+ /** Its Gravity Forms forms' descriptors, on a Site with `forms`. */
296
+ forms?: FormDescriptor[];
279
297
  } & Extended<X>;
280
298
  /** WordPress failed on the blocks, so this is the page without them. */
281
299
  blocksOmitted: boolean;
@@ -650,6 +668,36 @@ type CollectionTermListing = {
650
668
  kind: "found";
651
669
  terms: CollectionTermCount[];
652
670
  } | NotServed;
671
+ /** Search public content already delivered to this Site's publication store. */
672
+ interface SearchQuery {
673
+ /** Text to find in titles, excerpts or bodies, ignoring case and accents. Blank finds nothing. */
674
+ search: string;
675
+ /** Page from 1; defaults to 1. URL strings are accepted. */
676
+ page?: number | string;
677
+ /** Results per page, 1–100; defaults to 12. URL strings are accepted. */
678
+ perPage?: number | string;
679
+ /** Configured language slug; defaults to the Site's default language. */
680
+ language?: string;
681
+ }
682
+ interface SearchItem {
683
+ /** Canonical path on this Site. */
684
+ uri: string;
685
+ /** Plain text, suitable for escaped text rendering. */
686
+ title: string;
687
+ /** Plain text excerpt, or a bounded body-text fallback. */
688
+ excerpt: string;
689
+ /** Configured language slug, including on monolingual Sites. */
690
+ language: string;
691
+ }
692
+ interface SearchPage {
693
+ items: SearchItem[];
694
+ total: number;
695
+ page: number;
696
+ perPage: number;
697
+ /** Zero when nothing matches. */
698
+ totalPages: number;
699
+ }
700
+ type SearchDelivery = Found<SearchPage> | NotServed;
653
701
  //#endregion
654
702
  //#region frontend/src/server.d.ts
655
703
  interface ContentConfig<X = never> {
@@ -674,6 +722,14 @@ interface ContentConfig<X = never> {
674
722
  * delivery; absent, bodies are delivered as published.
675
723
  */
676
724
  consent?: ContentConsentConfig;
725
+ /**
726
+ * Headless Gravity Forms (ADR 0009). True, each front page, page, post and
727
+ * item is read with the public descriptors of the forms it references
728
+ * (gq-content's CMS forms module, active with Gravity Forms), stored with it
729
+ * as `forms`, and `publishedForm` finds them for the form endpoint. Absent,
730
+ * reads and stored bodies are unchanged.
731
+ */
732
+ forms?: boolean;
677
733
  /**
678
734
  * The Site's own content on its pages and posts: GraphQL fields the runtime
679
735
  * reads with each of them, and the value the Site makes of them, stored with
@@ -773,6 +829,15 @@ declare function createContentRuntime<X = never>(input: ContentConfig<X>): {
773
829
  }) => Promise<Response>;
774
830
  publishedHome: (siteOrigin?: string, language?: SiteLanguage) => Promise<HomeDelivery<X>>;
775
831
  publishedEntry: (path: string, siteOrigin?: string) => Promise<EntryDelivery<X>>;
832
+ /**
833
+ * A Gravity Forms form's descriptor as published with the page at a path
834
+ * (the publication store's, as the page is served), for the form endpoint
835
+ * (`createFormsServer`'s `descriptor`). Null when the page isn't served,
836
+ * doesn't reference the form, or the Site doesn't opt in to `forms`.
837
+ */
838
+ publishedForm: (path: string, formId: number, siteOrigin?: string) => Promise<FormDescriptor | null>;
839
+ /** Site-wide text search over delivered public content; never reads WordPress. */
840
+ search: (query: SearchQuery) => Promise<SearchDelivery>;
776
841
  /**
777
842
  * A page of a collection's items, from the publication store alone (never
778
843
  * WordPress), newest first: those with the given terms and text, in the
@@ -841,4 +906,4 @@ interface WordPressClient<X = unknown> {
841
906
  }): Promise<Found<SiteChrome> | Unavailable>;
842
907
  }
843
908
  //#endregion
844
- export { Found as $, htmlLang as A, DesignQueryRenderContext as At, designMenuItems as B, Moved as C, designThemeVariables as Ct, SqlStatement as D, DesignPostRecord as Dt, SqlDatabase as E, DesignPostContext as Et, DesignBlock as F, renderDesignPostBlock as Ft, CmsFailure as G, parseDesignBlocks as H, DesignBlockRenderer as I, renderDesignQuery as It, CollectionTerm as J, CmsFailureReason as K, DesignPattern as L, DesignTextContext as Lt, languageSubject as M, designPostContext as Mt, routeLanguage as N, designQueryItems as Nt, SiteLanguage as O, DesignPreloadedPosts as Ot, siteLanguages as P, preloadDesignQueryPosts as Pt, Extended as Q, DesignRenderContext as R, renderDesignText as Rt, HomePublication as S, designThemeStyleSheet as St, ContentBindings as T, resolvePresentationToken as Tt, renderDesignPattern as U, escapeHtml as V, Archives as W, DesignPresets as X, Delivery as Y, EntryContent as Z, EntryDelivery as _, presentationModel as _t, ContentExtension as a, Unavailable as at, HomeDelivery as b, DesignTheme as bt, createContentRuntime as c, WordPressPost as ct, CollectionListing as d, PresentationBreakpoint as dt, HomeContent as et, CollectionPage as f, PresentationModel as ft, DeliveryFailureReason as g, createPresentationModel as gt, CollectionTermListing as h, Property as ht, ContentConsentConfig as i, Translation as it, isLanguageHome as j, DesignQueryResolverOptions as jt, WordPressLanguages as k, DesignQueryItem as kt, Alternate as l, WordPressSettings as lt, CollectionTermCount as m, PresentationTheme as mt, CollectionConfig as n, PublishedEntry as nt, ContentRuntime as o, WordPressImage as ot, CollectionQuery as p, PresentationTargets as pt, CollectionMembership as q, ContentConfig as r, SiteChrome as rt, ExtensionNode as s, WordPressMenuItem as st, WordPressClient as t, Missing as tt, CollectionItem as u, Presentation as ut, EntryPage as v, presentationStyleSheet as vt, NotServed as w, resolveDesignTheme as wt, HomePage as x, DesignThemeOverrides as xt, EntryPublication as y, presentationVariables as yt, DesignRenderOptions as z, safeDesignRichText as zt };
909
+ export { Delivery as $, SqlDatabase as A, DesignPostContext as At, DesignBlockRenderer as B, renderDesignQuery as Bt, Moved as C, presentationVariables as Ct, SearchPage as D, designThemeVariables as Dt, SearchItem as E, designThemeStyleSheet as Et, isLanguageHome as F, DesignQueryResolverOptions as Ft, escapeHtml as G, DesignRenderContext as H, renderDesignText as Ht, languageSubject as I, designPostContext as It, Archives as J, parseDesignBlocks as K, routeLanguage as L, designQueryItems as Lt, SiteLanguage as M, DesignPreloadedPosts as Mt, WordPressLanguages as N, DesignQueryItem as Nt, SearchQuery as O, resolveDesignTheme as Ot, htmlLang as P, DesignQueryRenderContext as Pt, CollectionTerm as Q, siteLanguages as R, preloadDesignQueryPosts as Rt, HomePublication as S, presentationStyleSheet as St, SearchDelivery as T, DesignThemeOverrides as Tt, DesignRenderOptions as U, safeDesignRichText as Ut, DesignPattern as V, DesignTextContext as Vt, designMenuItems as W, CmsFailureReason as X, CmsFailure as Y, CollectionMembership as Z, EntryDelivery as _, PresentationTargets as _t, ContentExtension as a, Missing as at, HomeDelivery as b, createPresentationModel as bt, createContentRuntime as c, Translation as ct, CollectionListing as d, WordPressMenuItem as dt, DesignPresets as et, CollectionPage as f, WordPressPost as ft, DeliveryFailureReason as g, PresentationModel as gt, CollectionTermListing as h, PresentationBreakpoint as ht, ContentConsentConfig as i, HomeContent as it, SqlStatement as j, DesignPostRecord as jt, ContentBindings as k, resolvePresentationToken as kt, Alternate as l, Unavailable as lt, CollectionTermCount as m, Presentation as mt, CollectionConfig as n, Extended as nt, ContentRuntime as o, PublishedEntry as ot, CollectionQuery as p, WordPressSettings as pt, renderDesignPattern as q, ContentConfig as r, Found as rt, ExtensionNode as s, SiteChrome as st, WordPressClient as t, EntryContent as tt, CollectionItem as u, WordPressImage as ut, EntryPage as v, PresentationTheme as vt, NotServed as w, DesignTheme as wt, HomePage as x, presentationModel as xt, EntryPublication as y, Property as yt, DesignBlock as z, renderDesignPostBlock as zt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getquick/content",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "license": "UNLICENSED",
5
5
  "files": [
6
6
  "dist",
@@ -61,6 +61,22 @@
61
61
  "./design/client": {
62
62
  "types": "./dist/design-client.d.ts",
63
63
  "import": "./dist/design-client.js"
64
+ },
65
+ "./forms": {
66
+ "types": "./dist/forms.d.ts",
67
+ "import": "./dist/forms.js"
68
+ },
69
+ "./forms/astro": {
70
+ "types": "./dist/forms-astro.d.ts",
71
+ "import": "./dist/forms-astro.js"
72
+ },
73
+ "./forms/server": {
74
+ "types": "./dist/forms-server.d.ts",
75
+ "import": "./dist/forms-server.js"
76
+ },
77
+ "./forms/testing": {
78
+ "types": "./dist/forms-testing.d.ts",
79
+ "import": "./dist/forms-testing.js"
64
80
  }
65
81
  },
66
82
  "publishConfig": {