@bettercms-ai/convert 0.19.0 → 0.20.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/index.d.ts CHANGED
@@ -428,8 +428,12 @@ interface ConversionReceipt {
428
428
  * `ACTION_IS_EXPRESSION` the `<form>`'s `action` is code; replacing it would rewrite a program.
429
429
  * `DIALECT_UNSUPPORTED` the `<form>` is in a svelte/vue file, which this lane does not write.
430
430
  * `PARSE_ERROR` the file holding it did not parse.
431
+ * `FORM_NAME_AMBIGUOUS` an Astro form is wired by name, and two CMS forms share this one's.
432
+ * `HELPER_CONFLICT` `src/bcms-forms.ts` exists with bytes this package did not write.
433
+ * `FORM_WIRED_TO_OTHER_NAME` an Astro `<form>` is already wired by another name. Never rewritten:
434
+ * the name is what every environment building the repo shares.
431
435
  */
432
- type FormPendingReason = "FORM_NOT_IN_SOURCE" | "FORM_AMBIGUOUS" | "FIELD_UNMATCHED" | "ACTION_IS_EXPRESSION" | "DIALECT_UNSUPPORTED" | "PARSE_ERROR";
436
+ type FormPendingReason = "FORM_NOT_IN_SOURCE" | "FORM_AMBIGUOUS" | "FIELD_UNMATCHED" | "ACTION_IS_EXPRESSION" | "DIALECT_UNSUPPORTED" | "PARSE_ERROR" | "FORM_NAME_AMBIGUOUS" | "HELPER_CONFLICT" | "FORM_WIRED_TO_OTHER_NAME";
433
437
  interface PendingForm {
434
438
  id: string;
435
439
  name: string;
package/dist/index.js CHANGED
@@ -6443,6 +6443,114 @@ function readPlan(value) {
6443
6443
  // src/forms.ts
6444
6444
  import MagicString4 from "magic-string";
6445
6445
  var WIRABLE = ["html", "astro", "jsx"];
6446
+ var BY_NAME = ["astro"];
6447
+ var FORMS_HELPER_PATH = "src/bcms-forms.ts";
6448
+ var FORMS_HELPER_VERSION = 2;
6449
+ var FORMS_HELPER_HEADER = /^\/\/ @bettercms-ai\/convert forms-helper v(\d+)\r?\n/;
6450
+ var formsHelperVersion = (content) => {
6451
+ const m = FORMS_HELPER_HEADER.exec(content);
6452
+ return m ? Number(m[1]) : null;
6453
+ };
6454
+ var FORMS_HELPER_SOURCE = `// @bettercms-ai/convert forms-helper v${FORMS_HELPER_VERSION}
6455
+ // Written by @bettercms-ai/convert; a re-run rewrites it, so do not edit it.
6456
+ // Resolves a BetterCMS form by NAME at build, for the environment that is building this site: the id
6457
+ // from bcms-content.json (written into the tree before the build) and the endpoint from BCMS_API_URL.
6458
+ // The source names no host and no id, so every BetterCMS environment builds its own wiring.
6459
+ // A form it cannot resolve renders unwired (no action, no method, no id) with one warning line, and
6460
+ // the build goes on. Form NAMES are the contract between environments: rename a form on every one.
6461
+ import { existsSync, readFileSync } from "node:fs";
6462
+
6463
+ type SnapshotForm = { id: string; name: string; projectId?: string | null; ignoredAt?: string | null };
6464
+ type Snapshot = { projectId: string | null; forms: SnapshotForm[] };
6465
+ type Wiring = { id: string; action: string };
6466
+
6467
+ let snapshot: Snapshot | null | undefined;
6468
+ const resolved = new Map<string, Wiring | undefined>();
6469
+
6470
+ const env = (key: string): string | undefined => process.env[key] || process.env[\`PUBLIC_\${key}\`] || undefined;
6471
+
6472
+ const isForm = (value: unknown): value is SnapshotForm =>
6473
+ typeof value === "object" &&
6474
+ value !== null &&
6475
+ typeof (value as { id?: unknown }).id === "string" &&
6476
+ typeof (value as { name?: unknown }).name === "string";
6477
+
6478
+ function readSnapshot(): Snapshot | null {
6479
+ for (const file of ["bcms-content.json", "bcms-content/forms.json"]) {
6480
+ if (!existsSync(file)) continue;
6481
+ try {
6482
+ const data: unknown = JSON.parse(readFileSync(file, "utf8"));
6483
+ const record = (Array.isArray(data) ? { forms: data } : (data ?? {})) as { forms?: unknown; projectId?: unknown };
6484
+ const forms: unknown[] = Array.isArray(record.forms) ? record.forms : [];
6485
+ if (!forms.every(isForm)) throw new Error("a form entry has no id or name");
6486
+ return { projectId: typeof record.projectId === "string" ? record.projectId : null, forms };
6487
+ } catch (error) {
6488
+ // Read once per build, so this is the one line however many forms the site has.
6489
+ console.warn(\`[bcms-forms] FORMS_SNAPSHOT_UNREADABLE: \${file} could not be read (\${error instanceof Error ? error.message : String(error)}); every form renders unwired.\`);
6490
+ return null;
6491
+ }
6492
+ }
6493
+ return null;
6494
+ }
6495
+
6496
+ function unwired(code: string, name: string, why: string): undefined {
6497
+ console.warn(\`[bcms-forms] \${code}: form "\${name}" renders unwired; \${why}.\`);
6498
+ return undefined;
6499
+ }
6500
+
6501
+ function resolve(name: string): Wiring | undefined {
6502
+ if (snapshot === undefined) snapshot = readSnapshot();
6503
+ // No snapshot (a local dev build), or one it could not read: the form renders unwired, as before.
6504
+ if (snapshot === null) return undefined;
6505
+ const project = env("BCMS_PROJECT_ID") ?? snapshot.projectId;
6506
+ // The forms the conversion brief names from: this project's own, ignored ones left out.
6507
+ const own = snapshot.forms.filter((f) => !f.ignoredAt && (!project || f.projectId === project));
6508
+ const hits = own.filter((f) => f.name === name);
6509
+ const form = hits.length === 1 ? hits[0] : undefined;
6510
+ if (!form && hits.length > 1) {
6511
+ return unwired("BCMS_FORM_NAME_AMBIGUOUS", name, \`\${hits.length} forms in this project share that name; rename one in the Forms tab\`);
6512
+ }
6513
+ if (!form) {
6514
+ return unwired(
6515
+ "BCMS_FORM_NOT_FOUND",
6516
+ name,
6517
+ own.length === 0
6518
+ ? "this project has no forms yet; the release of this build derives them"
6519
+ : "no form in this project has that name; a name is shared by every environment, so rename the form on each one",
6520
+ );
6521
+ }
6522
+ const api = env("BCMS_API_URL");
6523
+ if (!api) return unwired("BCMS_FORM_API_URL_MISSING", name, "BCMS_API_URL is not set, so it has nowhere to post");
6524
+ return { id: form.id, action: \`\${api.replace(/\\/$/, "")}/api/v1/forms/public/\${form.id}/submissions\` };
6525
+ }
6526
+
6527
+ /** Resolved once per name, so a form's three attributes print one warning, not three. */
6528
+ function wiring(name: string): Wiring | undefined {
6529
+ if (!resolved.has(name)) resolved.set(name, resolve(name));
6530
+ return resolved.get(name);
6531
+ }
6532
+
6533
+ export const bcmsFormId = (name: string): string | undefined => wiring(name)?.id;
6534
+
6535
+ export const bcmsFormAction = (name: string): string | undefined => wiring(name)?.action;
6536
+
6537
+ /** "post" only for a form that resolved: an unwired form keeps no method, like its action and id. */
6538
+ export const bcmsFormMethod = (name: string): "post" | undefined => (wiring(name) ? "post" : undefined);
6539
+ `;
6540
+ var idCall = (form) => `bcmsFormId(${JSON.stringify(form.name)})`;
6541
+ var actionCall = (form) => `bcmsFormAction(${JSON.stringify(form.name)})`;
6542
+ var methodCall = (form) => `bcmsFormMethod(${JSON.stringify(form.name)})`;
6543
+ function calledName(code, fn) {
6544
+ const m = code === null ? null : new RegExp(`^${fn}\\s*\\(\\s*(["'\`])((?:\\\\.|(?!\\1)[^\\\\])*)\\1\\s*,?\\s*\\)$`).exec(code);
6545
+ if (!m) return null;
6546
+ const [, quote, raw] = m;
6547
+ if (quote === "`" && raw.includes("${")) return null;
6548
+ try {
6549
+ return JSON.parse(`"${raw.replace(/\\(['`])/g, "$1").replace(/(?<!\\)"/g, '\\"')}"`);
6550
+ } catch {
6551
+ return null;
6552
+ }
6553
+ }
6446
6554
  var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
6447
6555
  var FORM_SCRIPT = `<script>
6448
6556
  function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
@@ -6504,6 +6612,10 @@ function* elementsIn(nodes) {
6504
6612
  var childElements = (node) => node.children.flatMap(
6505
6613
  (child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
6506
6614
  );
6615
+ function exprOf(c, name) {
6616
+ const range = attrOf(c.el, name)?.expressionRange;
6617
+ return range ? c.content.slice(range.start + 1, range.end - 1).trim() : null;
6618
+ }
6507
6619
  var LANDMARK_TAGS = /* @__PURE__ */ new Set(["footer", "header", "nav", "main", "aside", "dialog"]);
6508
6620
  function landmarkOf(chain) {
6509
6621
  for (let i = chain.length - 1; i >= 0; i--) {
@@ -6525,7 +6637,7 @@ function* formsIn(nodes, chain = []) {
6525
6637
  }
6526
6638
  }
6527
6639
  var isComponentTag = (dialect, tag) => dialect !== "html" && /^[A-Z]/.test(tag);
6528
- function candidateOf(file, dialect, form, imported, landmark, content) {
6640
+ function candidateOf(file, dialect, form, imported, landmark, content, importAt) {
6529
6641
  const controls = /* @__PURE__ */ new Map();
6530
6642
  const wrapper = /* @__PURE__ */ new Map();
6531
6643
  const foreign = /* @__PURE__ */ new Set();
@@ -6566,7 +6678,7 @@ function candidateOf(file, dialect, form, imported, landmark, content) {
6566
6678
  content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\$]|\\.|\$(?!\{))*`/g, "")
6567
6679
  );
6568
6680
  const dynamic = all.some((control) => !foreign.has(control) && computed(attrOf(control, "name")));
6569
- return { file, dialect, el: form, controls, wrapper, foreign, landmark, dynamic };
6681
+ return { file, dialect, el: form, controls, wrapper, foreign, landmark, dynamic, content, importAt };
6570
6682
  }
6571
6683
  async function importedControls(source, parsed, dialect, project, cache) {
6572
6684
  const out = /* @__PURE__ */ new Map();
@@ -6599,11 +6711,20 @@ function wire(source, candidate, form) {
6599
6711
  else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
6600
6712
  else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
6601
6713
  };
6602
- set("action", form.submitUrl);
6603
- set("method", "post");
6604
- set("data-bcms-form", form.id);
6714
+ const setExpr = (name, code) => {
6715
+ const attr = attrOf(el, name);
6716
+ if (!attr) inserts.push(`${name}={${code}}`);
6717
+ else source.overwrite(attr.range.start, attr.range.end, `${name}={${code}}`);
6718
+ };
6719
+ const byName = BY_NAME.includes(dialect);
6720
+ if (byName) setExpr("action", actionCall(form));
6721
+ else set("action", form.submitUrl);
6722
+ if (byName) setExpr("method", methodCall(form));
6723
+ else set("method", "post");
6724
+ if (byName) setExpr("data-bcms-form", idCall(form));
6725
+ else set("data-bcms-form", form.id);
6605
6726
  if (form.successMessage) set("data-bcms-form-success", form.successMessage);
6606
- source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
6727
+ if (inserts.length > 0) source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
6607
6728
  for (const inner of elementsIn(el.children)) {
6608
6729
  for (const attr of inner.attrs) {
6609
6730
  if (!DECLARED_BINDING_ATTR.test(attr.name)) continue;
@@ -6660,12 +6781,20 @@ async function wireForms(briefIn, sources, options_ = {}) {
6660
6781
  }
6661
6782
  const imported = dialect === "html" ? /* @__PURE__ */ new Map() : await importedControls(source, parsed, dialect, project, componentFiles);
6662
6783
  for (const { el, landmark } of formsIn(parsed.roots)) {
6663
- candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content));
6784
+ candidates.push(candidateOf(source.path, dialect, el, imported, landmark, source.content, parsed.importAt));
6664
6785
  }
6665
6786
  }
6666
6787
  const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
6667
- const wiredTo = (c, f) => attrOf(c.el, "data-bcms-form")?.value === f.id;
6668
- const covers2 = (c, f) => c.dynamic && wiredTo(c, f) || f.fields.length > 0 && holds(c, f) === f.fields.length;
6788
+ const byId = (c, f) => !attrOf(c.el, "data-bcms-form")?.expression && attrOf(c.el, "data-bcms-form")?.value === f.id;
6789
+ const programAction = (c) => !!attrOf(c.el, "action")?.expression && calledName(exprOf(c, "action"), "bcmsFormAction") === null;
6790
+ const wiredTo = (c, f) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") === f.name || byId(c, f) && programAction(c) : byId(c, f);
6791
+ const wiredName = (c) => BY_NAME.includes(c.dialect) ? calledName(exprOf(c, "data-bcms-form"), "bcmsFormId") : null;
6792
+ const staticPost = (c) => {
6793
+ const method = attrOf(c.el, "method");
6794
+ return !!method && !method.expression && method.value.toLowerCase() === "post";
6795
+ };
6796
+ const claims = (c, f) => wiredTo(c, f) || byId(c, f);
6797
+ const covers2 = (c, f) => c.dynamic && claims(c, f) || f.fields.length > 0 && holds(c, f) === f.fields.length;
6669
6798
  const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
6670
6799
  const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
6671
6800
  const pinned = /* @__PURE__ */ new Map();
@@ -6709,7 +6838,14 @@ async function wireForms(briefIn, sources, options_ = {}) {
6709
6838
  const wanted = /* @__PURE__ */ new Map();
6710
6839
  for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
6711
6840
  const matched = [];
6841
+ const methodUpgrades = [];
6842
+ const pendedByName = /* @__PURE__ */ new Set();
6712
6843
  const pending = [];
6844
+ const existingHelper = sources.find((s) => samePath(s.path, FORMS_HELPER_PATH))?.content;
6845
+ const helperVersion = existingHelper === void 0 ? null : formsHelperVersion(existingHelper);
6846
+ const helperOk = existingHelper === void 0 || helperVersion !== null;
6847
+ const helperStale = existingHelper !== void 0 && helperVersion !== null && helperVersion <= FORMS_HELPER_VERSION && existingHelper !== FORMS_HELPER_SOURCE;
6848
+ let byNameOnSite = false;
6713
6849
  const onSite = [];
6714
6850
  let alreadyWired = 0;
6715
6851
  const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
@@ -6748,12 +6884,25 @@ async function wireForms(briefIn, sources, options_ = {}) {
6748
6884
  continue;
6749
6885
  }
6750
6886
  const candidate = list[0];
6887
+ const byName = BY_NAME.includes(candidate.dialect);
6888
+ if (byName && forms.filter((f) => f.name === form.name).length > 1) {
6889
+ pend2(form, "FORM_NAME_AMBIGUOUS", candidate.file, `more than one CMS form is named "${form.name}"; rename one in the Forms tab.`);
6890
+ continue;
6891
+ }
6751
6892
  if (wiredTo(candidate, form)) {
6752
6893
  alreadyWired++;
6753
6894
  onSite.push({ form, file: candidate.file });
6895
+ if (byName) byNameOnSite = true;
6896
+ if (wiredName(candidate) === form.name && staticPost(candidate) && helperOk) methodUpgrades.push({ form, candidate });
6754
6897
  continue;
6755
6898
  }
6756
- if (attrOf(candidate.el, "action")?.expression) {
6899
+ const other = wiredName(candidate);
6900
+ if (other !== null) {
6901
+ pendedByName.add(candidate);
6902
+ pend2(form, "FORM_WIRED_TO_OTHER_NAME", candidate.file, `its <form> is wired by name to "${other}", and a wiring by name is never rewritten: form names are shared by every environment, so give this form the same name on each one.`);
6903
+ continue;
6904
+ }
6905
+ if (programAction(candidate)) {
6757
6906
  pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
6758
6907
  continue;
6759
6908
  }
@@ -6765,18 +6914,46 @@ async function wireForms(briefIn, sources, options_ = {}) {
6765
6914
  pend2(form, "FIELD_UNMATCHED", candidate.file, `the control for ${misnamed.key} posts under an expression, so its name cannot be set to the CMS key.`);
6766
6915
  continue;
6767
6916
  }
6917
+ if (byName && !helperOk) {
6918
+ pend2(form, "HELPER_CONFLICT", FORMS_HELPER_PATH, `${FORMS_HELPER_PATH} exists and is not the file @bettercms-ai/convert writes, so nothing may import it. Move it and re-run.`);
6919
+ continue;
6920
+ }
6768
6921
  matched.push({ form, candidate });
6769
6922
  onSite.push({ form, file: candidate.file });
6923
+ if (byName) byNameOnSite = true;
6770
6924
  }
6771
6925
  const contents = new Map(sources.map((s) => [s.path, s.content]));
6772
6926
  const byFile = /* @__PURE__ */ new Map();
6773
- for (const m of matched) byFile.set(m.candidate.file, [...byFile.get(m.candidate.file) ?? [], m]);
6927
+ const editsOf = (path) => {
6928
+ if (!byFile.has(path)) byFile.set(path, { wire: [], upgrade: [] });
6929
+ return byFile.get(path);
6930
+ };
6931
+ for (const m of matched) editsOf(m.candidate.file).wire.push(m);
6932
+ for (const m of methodUpgrades) editsOf(m.candidate.file).upgrade.push(m);
6774
6933
  const files = [];
6775
- for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
6934
+ for (const [path, { wire: list, upgrade }] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
6776
6935
  const content = contents.get(path);
6777
6936
  const source = new MagicString4(content);
6778
6937
  for (const { form, candidate } of list) wire(source, candidate, form);
6779
- if (!content.includes("form[data-bcms-form]")) {
6938
+ for (const { form, candidate } of upgrade) {
6939
+ const method = attrOf(candidate.el, "method");
6940
+ source.overwrite(method.range.start, method.range.end, `method={${methodCall(form)}}`);
6941
+ }
6942
+ const first = (list[0] ?? upgrade[0]).candidate;
6943
+ if (BY_NAME.includes(first.dialect) && first.importAt) {
6944
+ const at = first.importAt;
6945
+ const helper = relativeImport(path, FORMS_HELPER_PATH.replace(/\.ts$/, ""));
6946
+ const names = !/\bbcmsFormId\b/.test(content) ? "bcmsFormAction, bcmsFormId, bcmsFormMethod" : !/\bbcmsFormMethod\b/.test(content) ? "bcmsFormMethod" : null;
6947
+ if (names) {
6948
+ const line = `import { ${names} } from "${helper}";`;
6949
+ if (at.wrap) source.appendLeft(at.at, `${at.wrap.before}${line}${at.wrap.after}`);
6950
+ else if (at.at === 0) source.appendLeft(0, `${line}
6951
+ `);
6952
+ else source.appendLeft(at.at, `
6953
+ ${line}`);
6954
+ }
6955
+ }
6956
+ if (list.length > 0 && !content.includes("form[data-bcms-form]")) {
6780
6957
  const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
6781
6958
  const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
6782
6959
  source.appendRight(last.candidate.el.end, `
@@ -6784,6 +6961,10 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6784
6961
  }
6785
6962
  files.push({ path, operation: "modify", content: source.toString() });
6786
6963
  }
6964
+ const helperAdded = existingHelper === void 0 && (matched.some((m) => BY_NAME.includes(m.candidate.dialect)) || methodUpgrades.length > 0);
6965
+ if (helperAdded) files.push({ path: FORMS_HELPER_PATH, operation: "add", content: FORMS_HELPER_SOURCE });
6966
+ const helperUpgraded = helperStale && byNameOnSite;
6967
+ if (helperUpgraded) files.push({ path: FORMS_HELPER_PATH, operation: "modify", content: FORMS_HELPER_SOURCE });
6787
6968
  const notes = onSite.filter(({ form }) => form.status === "draft").map(({ form }) => `publish form ${form.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
6788
6969
  for (const { form, candidate } of matched) {
6789
6970
  const tie = beat.get(candidate);
@@ -6799,6 +6980,13 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6799
6980
  );
6800
6981
  }
6801
6982
  }
6983
+ for (const c of candidates) {
6984
+ const name = wiredName(c);
6985
+ if (name === null || pendedByName.has(c) || forms.some((f) => f.name === name)) continue;
6986
+ notes.push(
6987
+ `${c.file}: its <form> is wired by name to "${name}", which no form of this project has, so this environment's build renders it unwired; it was not rewritten \u2014 form names are shared by every environment, so give the form that name here too.`
6988
+ );
6989
+ }
6802
6990
  for (const { form, candidate } of matched) {
6803
6991
  const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
6804
6992
  if (keys.length > 0) {
@@ -6816,8 +7004,8 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6816
7004
  paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
6817
7005
  occurrences: { located: 0, rewritten: 0 },
6818
7006
  files: [],
6819
- helpers: [],
6820
- helperUpgraded: false,
7007
+ helpers: helperAdded || helperUpgraded ? [FORMS_HELPER_PATH] : [],
7008
+ helperUpgraded,
6821
7009
  dynamicBindings: [],
6822
7010
  forms: assertForms(forms.length, {
6823
7011
  wired: matched.length,
@@ -6921,7 +7109,7 @@ var CONTEXT_TAGS = /* @__PURE__ */ new Set([
6921
7109
  var BOUND_ATTRS = ["data-bcms-field", "data-bcms-layout-field"];
6922
7110
  var isDecorativeGlyph = (text) => {
6923
7111
  const t = flat2(text);
6924
- return t === "" || !/[\p{L}\p{N}]/u.test(t) || new RegExp("^\\p{Nd}{1,2}\\s*[/.)\\-\u2013\u2014]?$", "u").test(t);
7112
+ return t === "" || !/[\p{L}\p{N}]/u.test(t) || new RegExp("^(?:0\\p{Nd}|\\p{Nd}{1,2}\\s*[/.)\\-\u2013\u2014])$", "u").test(t);
6925
7113
  };
6926
7114
  var PLATFORM_BADGE = /^made with\b/iu;
6927
7115
  var MAX_BUCKETS = 12;