@getquick/content 0.3.2 → 0.4.1

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/dist/server.js CHANGED
@@ -1,4 +1,6 @@
1
- import { D as renderWordPressBlocks, E as parseWordPressBlocks, T as hasVideoHero, V as enhanceDesignNavigationHtml, Z as resolveDesignTheme, _ as uriPath, a as siteLanguages, c as parseDesignBlocks, d as decodeEntities, et as assertDesignBlocks, g as stripHtml, h as routeOf, i as routeLanguage, l as renderDesignPattern, n as isLanguageHome, p as menuItemHref, r as languageSubject, tt as enhanceDesignCoverVideosHtml, u as cmsTime, w as hasDesignCarousel, x as preloadDesignQueryPosts } from "./site-language-BcPac0Ia.js";
1
+ import { n as parseFragment } from "./dist-5jNKLmbn.js";
2
+ import { D as renderWordPressBlocks, E as parseWordPressBlocks, T as hasVideoHero, V as enhanceDesignNavigationHtml, Z as resolveDesignTheme, _ as uriPath, a as siteLanguages, c as parseDesignBlocks, d as decodeEntities, et as assertDesignBlocks, g as stripHtml, h as routeOf, i as routeLanguage, l as renderDesignPattern, n as isLanguageHome, nt as safeContentUrl, p as menuItemHref, r as languageSubject, tt as enhanceDesignCoverVideosHtml, u as cmsTime, w as hasDesignCarousel, x as preloadDesignQueryPosts } from "./site-language-BCGjkpWY.js";
3
+ import { d as parseFormDescriptors, o as findForm } from "./descriptor-x8kHFQ79.js";
2
4
  import { t as bearerMatches } from "./bearer-DMOCoSci.js";
3
5
  import { _ as consentConfiguration } from "./state-BYWc5TUB.js";
4
6
  import { i as rewriteEmbeds, l as parseLegalLanguage } from "./embeds-CJJemAmE.js";
@@ -3816,6 +3818,11 @@ function publicationStore(db) {
3816
3818
  failedEvents: count(failed)
3817
3819
  };
3818
3820
  },
3821
+ async searchRows(home, chrome) {
3822
+ const { results } = await guard("be read", () => db.prepare(`SELECT key, state, format, body FROM publications
3823
+ WHERE key IN (?, ?) OR (substr(key, 1, 6) = 'entry:' AND state = 'published')`).bind(home, chrome).all());
3824
+ return results;
3825
+ },
3819
3826
  async listItems(filter, limit, offset) {
3820
3827
  const { where, values } = itemsWhere(filter);
3821
3828
  const { results } = await guard("be read", () => db.prepare(`SELECT key, body, modified_at, count(*) OVER () AS total FROM publications p
@@ -4955,6 +4962,8 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
4955
4962
  }
4956
4963
  return parsed;
4957
4964
  }) } : {};
4965
+ /** Gravity Forms descriptors stored with an entry (a Site with `forms`); invalid ones are dropped. */
4966
+ const forms = arrayType(unknownType()).optional().transform((value) => value === void 0 ? void 0 : parseFormDescriptors(value));
4958
4967
  const homePublication = objectType({
4959
4968
  settings: objectType({
4960
4969
  title: stringType(),
@@ -4966,6 +4975,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
4966
4975
  hasVideoHero: booleanType(),
4967
4976
  databaseId: numberType().int().positive().optional(),
4968
4977
  designBlocks: designBlocksSchema.optional(),
4978
+ forms,
4969
4979
  ...extended
4970
4980
  }),
4971
4981
  spacingSizes,
@@ -5006,6 +5016,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
5006
5016
  hasVideoHero: booleanType(),
5007
5017
  databaseId: numberType().int().positive().optional(),
5008
5018
  designBlocks: designBlocksSchema.optional(),
5019
+ forms,
5009
5020
  spacingSizes,
5010
5021
  colors,
5011
5022
  theme: designThemeSchema.optional(),
@@ -5995,6 +6006,89 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
5995
6006
  pages: Math.ceil(found.total / perPage)
5996
6007
  };
5997
6008
  }
6009
+ /** Search a single snapshot, validating before counting or paging it. Never reads WordPress. */
6010
+ async function search(query) {
6011
+ const subject = "site search";
6012
+ const language = query.language === void 0 ? defaultLanguage : languages.find((each) => each.slug === query.language);
6013
+ if (!language) throw new Error(`${query.language} isn't one of the Site's languages`);
6014
+ const page = searchWhole("page", query.page, 1);
6015
+ const perPage = Math.min(100, searchWhole("perPage", query.perPage, 12));
6016
+ if (typeof query.search !== "string") throw new Error("search must be text");
6017
+ const needle = searchText(query.search);
6018
+ const store = await listingStore(subject);
6019
+ if ("kind" in store) return store;
6020
+ let rows;
6021
+ try {
6022
+ rows = await store.searchRows(homeKey(language), chromeKey(language));
6023
+ } catch (error) {
6024
+ if (!(error instanceof StoreFailure)) throw error;
6025
+ return notServed(subject, "store", error.message);
6026
+ }
6027
+ const unreadable = () => notServed(subject, "not-ready", "Search requires readable publications and this language's home and chrome in the publication store");
6028
+ const home = rows.find((row) => row.key === homeKey(language));
6029
+ const chrome = rows.find((row) => row.key === chromeKey(language));
6030
+ if (!home || !chrome || home.format !== 1 || chrome.format !== 1) return unreadable();
6031
+ const parseBody = (body, parse) => {
6032
+ try {
6033
+ return body === null ? void 0 : parse(JSON.parse(body));
6034
+ } catch {
6035
+ return;
6036
+ }
6037
+ };
6038
+ if (chrome.state !== "published" || !parseBody(chrome.body, parseChrome)) return unreadable();
6039
+ if (![
6040
+ "published",
6041
+ "missing",
6042
+ "withdrawn"
6043
+ ].includes(home.state)) return unreadable();
6044
+ const matches = [];
6045
+ const add = (uri, title, excerpt, body, date) => {
6046
+ const plainTitle = searchPlainText(title);
6047
+ const plainExcerpt = searchPlainText(excerpt);
6048
+ const plainBody = searchPlainText(body);
6049
+ if (needle && searchText(`${plainTitle} ${plainExcerpt} ${plainBody}`).includes(needle)) matches.push({
6050
+ uri,
6051
+ title: plainTitle,
6052
+ excerpt: Array.from(plainExcerpt || plainBody).slice(0, 240).join(""),
6053
+ language: language.slug,
6054
+ date
6055
+ });
6056
+ };
6057
+ if (home.state === "published") {
6058
+ const content = parseBody(home.body, parseHome);
6059
+ if (!content) return unreadable();
6060
+ add(language.home, content.page.title, "", content.page.content, "");
6061
+ }
6062
+ for (const row of rows) {
6063
+ if (!row.key.startsWith("entry:")) continue;
6064
+ const route = row.key.slice(6);
6065
+ if (isLanguageHome$1(route) || routeLanguage$1(route).slug !== language.slug) continue;
6066
+ if (row.format !== 1) return unreadable();
6067
+ const entry = parseBody(row.body, parseEntry);
6068
+ if (!entry) return unreadable();
6069
+ if (!inLanguage(entry, language)) continue;
6070
+ if (entry.collection && !Object.hasOwn(collections, entry.collection.name)) continue;
6071
+ if (routeOf(entry.uri) !== route || !entry.uri.startsWith("/") || !safeContentUrl(entry.uri)) return unreadable();
6072
+ add(entry.uri, entry.title, entry.excerpt, entry.content, entry.date);
6073
+ }
6074
+ matches.sort((a, b) => a.date === b.date ? a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0 : a.date > b.date ? -1 : 1);
6075
+ const total = matches.length;
6076
+ return {
6077
+ kind: "found",
6078
+ content: {
6079
+ items: matches.slice((page - 1) * perPage, page * perPage).map(({ date: _date, ...item }) => item),
6080
+ total,
6081
+ page,
6082
+ perPage,
6083
+ totalPages: Math.ceil(total / perPage)
6084
+ }
6085
+ };
6086
+ }
6087
+ function searchWhole(option, value, fallback) {
6088
+ const number = value === void 0 || value === "" ? fallback : Number(value);
6089
+ if (!Number.isSafeInteger(number) || number < 1 || number > Number.MAX_SAFE_INTEGER / 100) throw new Error(`${option} must be a positive safe whole number`);
6090
+ return number;
6091
+ }
5998
6092
  /** A listing's whole-number option (as a number, or a string of one, from a URL), else its default. */
5999
6093
  function whole(option, value, fallback) {
6000
6094
  if (value === void 0 || value === null || value === "") return fallback;
@@ -6034,7 +6128,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
6034
6128
  } catch {
6035
6129
  return notServed(subject, "store", "The publication store couldn't be loaded");
6036
6130
  }
6037
- if (!store) return notServed(subject, "not-ready", "this Frontend has no publication store bound (PUBLICATION_DB); collections are only listed from one");
6131
+ if (!store) return notServed(subject, "not-ready", "this Frontend has no publication store bound (PUBLICATION_DB); listings require one");
6038
6132
  return store;
6039
6133
  }
6040
6134
  /**
@@ -6096,6 +6190,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
6096
6190
  publishedEntry: (path, siteOrigin) => safely(async () => html.entry(await publishedEntry(path, siteOrigin), routeLanguage$1(routeOf(path))), () => notServed("the entry", "store", "The publication store couldn't be loaded")),
6097
6191
  pageChrome: (siteOrigin) => safely(async () => html.chrome(await pageChrome(siteOrigin)), () => null),
6098
6192
  designPresets: () => safely(() => pageDesign(), () => null),
6193
+ search,
6099
6194
  list,
6100
6195
  terms,
6101
6196
  archiveUri: (collection) => {
@@ -6108,6 +6203,21 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
6108
6203
  function searchText(text) {
6109
6204
  return text.normalize("NFD").replace(/\p{M}/gu, "").replace(/[\u2018\u2019\u201A\u201B\u2032]/g, "'").replace(/[\u201C\u201D\u201E\u2033]/g, "\"").toLowerCase().replace(/\s+/g, " ").trim();
6110
6205
  }
6206
+ const searchTextBoundary = /^(address|article|aside|blockquote|br|dd|div|dl|dt|figcaption|figure|footer|h[1-6]|header|hr|li|main|nav|ol|p|pre|section|table|td|th|tr|ul)$/;
6207
+ /** Visible text for search and escaped result rendering, excluding script/style contents. */
6208
+ function searchPlainText(value) {
6209
+ const text = (node) => {
6210
+ if (node.nodeName === "#text" && "value" in node) return node.value;
6211
+ if ("tagName" in node && [
6212
+ "script",
6213
+ "style",
6214
+ "template"
6215
+ ].includes(node.tagName)) return "";
6216
+ const children = "childNodes" in node ? node.childNodes.map(text).join("") : "";
6217
+ return "tagName" in node && searchTextBoundary.test(node.tagName) ? ` ${children} ` : children;
6218
+ };
6219
+ return text(parseFragment(value)).replace(/\s+/g, " ").trim();
6220
+ }
6111
6221
  //#endregion
6112
6222
  //#region frontend/src/wordpress.ts
6113
6223
  /** The origins a link into this Site may use: WordPress's, and the Frontend's when given. */
@@ -6133,6 +6243,13 @@ function configuredWordPress(config, languages) {
6133
6243
  const fields = extensionSelection(kind, collection);
6134
6244
  return fields ? `\n... @include(if: $withBlocks) {\n${fields}\n}` : "";
6135
6245
  };
6246
+ /**
6247
+ * The public descriptors of the Gravity Forms forms an entry references
6248
+ * (gq-content's CMS forms module), read with the blocks on a Site that
6249
+ * opts in to forms. Without the option the queries are unchanged.
6250
+ */
6251
+ const formsField = config.forms ? "\n getquickForms @include(if: $withBlocks)" : "";
6252
+ const formsJson = stringType().nullish().transform((value) => value == null ? void 0 : parseFormDescriptors(value));
6136
6253
  const image = objectType({
6137
6254
  sourceUrl: stringType(),
6138
6255
  altText: stringType().nullable()
@@ -6159,6 +6276,7 @@ function configuredWordPress(config, languages) {
6159
6276
  id: stringType().nullable().optional(),
6160
6277
  databaseId: numberType().int().positive().optional(),
6161
6278
  getquickBlocksJson: designBlocksJsonSchema,
6279
+ getquickForms: formsJson.optional(),
6162
6280
  isFrontPage: booleanType(),
6163
6281
  title: stringType().nullable(),
6164
6282
  content: stringType().nullable(),
@@ -6170,6 +6288,7 @@ function configuredWordPress(config, languages) {
6170
6288
  id: stringType(),
6171
6289
  databaseId: numberType().int().positive().optional(),
6172
6290
  getquickBlocksJson: designBlocksJsonSchema,
6291
+ getquickForms: formsJson.optional(),
6173
6292
  title: stringType().nullable(),
6174
6293
  content: stringType().nullable(),
6175
6294
  uri: stringType(),
@@ -6205,6 +6324,7 @@ function configuredWordPress(config, languages) {
6205
6324
  __typename: stringType(),
6206
6325
  databaseId: numberType().int().positive().optional(),
6207
6326
  getquickBlocksJson: designBlocksJsonSchema,
6327
+ getquickForms: formsJson.optional(),
6208
6328
  id: stringType(),
6209
6329
  uri: stringType().nullable(),
6210
6330
  status: stringType().nullable(),
@@ -6314,7 +6434,7 @@ function configuredWordPress(config, languages) {
6314
6434
  content
6315
6435
  blocks(attributes: true, htmlContent: true, dynamicContent: true, postTemplate: false)
6316
6436
  @include(if: $withBlocks)
6317
- isFrontPage${extensionFields("page")}
6437
+ isFrontPage${formsField}${extensionFields("page")}
6318
6438
  }
6319
6439
  }
6320
6440
  designTokens {
@@ -6346,7 +6466,7 @@ function configuredWordPress(config, languages) {
6346
6466
  content
6347
6467
  blocks(attributes: true, htmlContent: true, dynamicContent: true, postTemplate: false)
6348
6468
  @include(if: $withBlocks)
6349
- isFrontPage${extensionFields("page")}
6469
+ isFrontPage${formsField}${extensionFields("page")}
6350
6470
  }
6351
6471
  }
6352
6472
  designTokens {
@@ -6448,7 +6568,7 @@ function configuredWordPress(config, languages) {
6448
6568
  sourceUrl
6449
6569
  altText
6450
6570
  }
6451
- }${extensionFields("post")}
6571
+ }${formsField}${extensionFields("post")}
6452
6572
  }
6453
6573
  pageBy(uri: $uri) {
6454
6574
  id
@@ -6467,7 +6587,7 @@ function configuredWordPress(config, languages) {
6467
6587
  sourceUrl
6468
6588
  altText
6469
6589
  }
6470
- }${extensionFields("page")}
6590
+ }${formsField}${extensionFields("page")}
6471
6591
  }
6472
6592
  }
6473
6593
  `;
@@ -6517,7 +6637,7 @@ function configuredWordPress(config, languages) {
6517
6637
  isRestricted
6518
6638
  modifiedGmt
6519
6639
  blocks(attributes: true, htmlContent: true, dynamicContent: true, postTemplate: false)
6520
- @include(if: $withBlocks)
6640
+ @include(if: $withBlocks)${formsField}
6521
6641
  }
6522
6642
  ... on NodeWithTitle {
6523
6643
  title
@@ -6600,6 +6720,7 @@ function configuredWordPress(config, languages) {
6600
6720
  id: post.id,
6601
6721
  ...post.databaseId !== void 0 ? { databaseId: post.databaseId } : {},
6602
6722
  ...post.getquickBlocksJson !== void 0 ? { designBlocks: post.getquickBlocksJson } : {},
6723
+ ...config.forms && post.getquickForms !== void 0 ? { forms: post.getquickForms } : {},
6603
6724
  title: post.title ?? "",
6604
6725
  excerpt: post.excerpt ?? "",
6605
6726
  content: post.content ?? "",
@@ -6730,6 +6851,9 @@ function configuredWordPress(config, languages) {
6730
6851
  function isServerError(error) {
6731
6852
  return error instanceof CmsFailureError && error.failure.reason === "http" && (error.failure.httpStatus ?? 0) >= 500;
6732
6853
  }
6854
+ /** Optional fields (getquickBlocksJson, getquickForms) this CMS has answered it lacks. */
6855
+ const absentFields = /* @__PURE__ */ new Set();
6856
+ const OPTIONAL_FIELD_ERROR = /^Cannot query field "(getquickBlocksJson|getquickForms)" on type "(?:Post|Page|ContentNode)"\.(?: Did you mean [^\n]+)?$/;
6733
6857
  /**
6734
6858
  * The CMS can fail on one page's blocks (a 502 for a block's attributes) while
6735
6859
  * the rest of it is fine: read it again without them rather than fail it. Only
@@ -6737,12 +6861,17 @@ function configuredWordPress(config, languages) {
6737
6861
  * too is a failure, never absence.
6738
6862
  */
6739
6863
  async function queryWithBlockRecovery(schema, queryText, variables = {}) {
6864
+ for (const field of absentFields) queryText = queryText.replaceAll(`${field} @include(if: $withBlocks)`, "");
6740
6865
  const read = async (readVariables) => {
6741
6866
  try {
6742
6867
  return await query(schema, queryText, readVariables);
6743
6868
  } catch (error) {
6744
- if (!(error instanceof CmsFailureError) || error.failure.reason !== "graphql" || !error.graphqlErrors?.length || !error.graphqlErrors.every((message) => /^Cannot query field "getquickBlocksJson" on type "(?:Post|Page)"\.(?: Did you mean [^\n]+)?$/.test(message))) throw error;
6745
- queryText = queryText.replace(/getquickBlocksJson @include\(if: \$withBlocks\)/g, "");
6869
+ if (!(error instanceof CmsFailureError) || error.failure.reason !== "graphql" || !error.graphqlErrors?.length || !error.graphqlErrors.every((message) => OPTIONAL_FIELD_ERROR.test(message))) throw error;
6870
+ for (const message of error.graphqlErrors) {
6871
+ const field = OPTIONAL_FIELD_ERROR.exec(message)[1];
6872
+ absentFields.add(field);
6873
+ queryText = queryText.replaceAll(`${field} @include(if: $withBlocks)`, "");
6874
+ }
6746
6875
  return query(schema, queryText, readVariables);
6747
6876
  }
6748
6877
  };
@@ -6815,6 +6944,7 @@ function configuredWordPress(config, languages) {
6815
6944
  page: {
6816
6945
  ...node.databaseId !== void 0 ? { databaseId: node.databaseId } : {},
6817
6946
  ...node.getquickBlocksJson !== void 0 ? { designBlocks: node.getquickBlocksJson } : {},
6947
+ ...config.forms && node.getquickForms !== void 0 ? { forms: node.getquickForms } : {},
6818
6948
  title: node.title ?? "",
6819
6949
  content: containsVideoHero || hasDesignCarousel(blocks) ? renderWordPressBlocks(blocks) : node.content ?? "",
6820
6950
  hasVideoHero: containsVideoHero,
@@ -7269,6 +7399,25 @@ function createContentRuntime(input) {
7269
7399
  publishedHome: delivery.publishedHome,
7270
7400
  publishedEntry: delivery.publishedEntry,
7271
7401
  /**
7402
+ * A Gravity Forms form's descriptor as published with the page at a path
7403
+ * (the publication store's, as the page is served), for the form endpoint
7404
+ * (`createFormsServer`'s `descriptor`). Null when the page isn't served,
7405
+ * doesn't reference the form, or the Site doesn't opt in to `forms`.
7406
+ */
7407
+ publishedForm: async (path, formId, siteOrigin) => {
7408
+ if (!config.forms) return null;
7409
+ const pathname = new URL(path, "https://site.invalid").pathname;
7410
+ const route = isLanguageHome(`${pathname}/`, languages) ? `${pathname}/` : pathname;
7411
+ if (isLanguageHome(route, languages)) {
7412
+ const home = await delivery.publishedHome(siteOrigin, routeLanguage(route, languages));
7413
+ return home.kind === "found" ? findForm(home.content.home.page.forms, formId) : null;
7414
+ }
7415
+ const entry = await delivery.publishedEntry(route, siteOrigin);
7416
+ return entry.kind === "found" ? findForm(entry.content.entry.forms, formId) : null;
7417
+ },
7418
+ /** Site-wide text search over delivered public content; never reads WordPress. */
7419
+ search: delivery.search,
7420
+ /**
7272
7421
  * A page of a collection's items, from the publication store alone (never
7273
7422
  * WordPress), newest first: those with the given terms and text, in the
7274
7423
  * language. Unavailable when the store can't be read, or there is none.