@bettercms-ai/convert 0.8.0 → 0.10.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/CHANGELOG.md +51 -0
- package/README.md +65 -0
- package/dist/cli.js +529 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +172 -4
- package/dist/index.js +395 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import MagicString6 from "magic-string";
|
|
3
3
|
|
|
4
4
|
// src/brief.ts
|
|
5
5
|
import { createHash } from "crypto";
|
|
@@ -2560,8 +2560,18 @@ var IMAGE_SOURCES = {
|
|
|
2560
2560
|
video: ["poster"]
|
|
2561
2561
|
};
|
|
2562
2562
|
var isImageSource = (site) => site.where === "attr" && (IMAGE_SOURCES[site.tag.toLowerCase()] ?? []).includes(site.attr ?? "");
|
|
2563
|
+
var ownsSubtree = (kind, site, inline) => kind === "richtext" && (inline === true ? !site.nonHtml : !(site.mixed && site.textRange !== null));
|
|
2563
2564
|
var norm = (value) => decodeEntities(value.normalize("NFC")).replace(/\s+/g, " ").trim();
|
|
2564
|
-
var BCMS_ATTRS = [
|
|
2565
|
+
var BCMS_ATTRS = [
|
|
2566
|
+
"data-bcms-field",
|
|
2567
|
+
"data-bcms-kind",
|
|
2568
|
+
"data-bcms-props",
|
|
2569
|
+
"data-bcms-layout-field",
|
|
2570
|
+
// The forms lane's two. Listed here so `collectSites` never matches a brief literal against a
|
|
2571
|
+
// marker this package itself wrote — a form id or a field key is not the page's copy.
|
|
2572
|
+
"data-bcms-form",
|
|
2573
|
+
"data-bcms-form-field"
|
|
2574
|
+
];
|
|
2565
2575
|
var attrInsertAfter = (attrs, start, tag) => attrs.length > 0 ? Math.max(...attrs.map((a) => a.range.end)) : start + 1 + tag.length;
|
|
2566
2576
|
function openTagEnd(content, start) {
|
|
2567
2577
|
let quote = null;
|
|
@@ -2583,6 +2593,7 @@ function openTagEnd(content, start) {
|
|
|
2583
2593
|
}
|
|
2584
2594
|
return content.length;
|
|
2585
2595
|
}
|
|
2596
|
+
var OPAQUE_TAGS = /* @__PURE__ */ new Set(["pre"]);
|
|
2586
2597
|
var isBlank = (value) => value.trim() === "";
|
|
2587
2598
|
function renderedText(node) {
|
|
2588
2599
|
if (node.type === "text") return node.value;
|
|
@@ -2590,6 +2601,11 @@ function renderedText(node) {
|
|
|
2590
2601
|
return "";
|
|
2591
2602
|
}
|
|
2592
2603
|
var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
|
|
2604
|
+
function nonHtmlSubtree(node) {
|
|
2605
|
+
return node.children.some(
|
|
2606
|
+
(c) => c.type === "expr" || c.type === "element" && (!isIntrinsic(c.tag) || c.tag === "slot" || nonHtmlSubtree(c))
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2593
2609
|
function soleTextChild(node) {
|
|
2594
2610
|
const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
|
|
2595
2611
|
return texts.length === 1 ? texts[0] : null;
|
|
@@ -2626,10 +2642,12 @@ function collectSites(roots, literals) {
|
|
|
2626
2642
|
if (node.type !== "element") return matched;
|
|
2627
2643
|
const attrs = attrsOf(node);
|
|
2628
2644
|
let index = 0;
|
|
2629
|
-
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2632
|
-
|
|
2645
|
+
if (!OPAQUE_TAGS.has(node.tag)) {
|
|
2646
|
+
for (const child of node.children) {
|
|
2647
|
+
const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
|
|
2648
|
+
if (child.type === "element") index++;
|
|
2649
|
+
for (const hit of visit(child, childWalk)) matched.add(hit);
|
|
2650
|
+
}
|
|
2633
2651
|
}
|
|
2634
2652
|
if (node.raw) {
|
|
2635
2653
|
const text2 = norm(renderedTextRaw(node));
|
|
@@ -2655,6 +2673,7 @@ function collectSites(roots, literals) {
|
|
|
2655
2673
|
textRange: soleTextChild(node)?.range ?? null,
|
|
2656
2674
|
literal,
|
|
2657
2675
|
partial: false,
|
|
2676
|
+
...nonHtmlSubtree(node) ? { nonHtml: true } : {},
|
|
2658
2677
|
ancestors: walk.ancestors,
|
|
2659
2678
|
siblingIndex: walk.siblingIndex
|
|
2660
2679
|
});
|
|
@@ -2680,6 +2699,7 @@ function collectSites(roots, literals) {
|
|
|
2680
2699
|
textRange: own.range,
|
|
2681
2700
|
literal,
|
|
2682
2701
|
partial: false,
|
|
2702
|
+
...nonHtmlSubtree(node) ? { nonHtml: true } : {},
|
|
2683
2703
|
ancestors: walk.ancestors,
|
|
2684
2704
|
siblingIndex: walk.siblingIndex
|
|
2685
2705
|
});
|
|
@@ -2730,10 +2750,12 @@ function partials(node, literals, walk, out) {
|
|
|
2730
2750
|
if (node.type !== "element" || node.raw) return false;
|
|
2731
2751
|
let deeper = false;
|
|
2732
2752
|
let index = 0;
|
|
2733
|
-
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2753
|
+
if (!OPAQUE_TAGS.has(node.tag)) {
|
|
2754
|
+
for (const child of node.children) {
|
|
2755
|
+
const childWalk = { ancestors: [...walk.ancestors, node.tag], siblingIndex: index };
|
|
2756
|
+
if (child.type === "element") index++;
|
|
2757
|
+
if (partials(child, literals, childWalk, out)) deeper = true;
|
|
2758
|
+
}
|
|
2737
2759
|
}
|
|
2738
2760
|
if (deeper) return true;
|
|
2739
2761
|
const text = norm(renderedText(node));
|
|
@@ -4373,14 +4395,23 @@ function isKnownHelper(content, dialect) {
|
|
|
4373
4395
|
|
|
4374
4396
|
// src/locate.ts
|
|
4375
4397
|
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
4376
|
-
var stripTags = (value) => value.replace(/<br\s*\/?>/gi,
|
|
4398
|
+
var stripTags = (value, br = " ") => value.replace(/<br\s*\/?>/gi, br).replace(/<[^>]*>/g, "");
|
|
4377
4399
|
function locate(brief, sources, options) {
|
|
4378
4400
|
const flattened = sources.map((s) => {
|
|
4379
4401
|
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
4380
4402
|
return {
|
|
4381
4403
|
path: s.path,
|
|
4382
4404
|
content: flat(readable),
|
|
4383
|
-
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
4405
|
+
text: options?.stripTags ? flat(stripTags(readable)) : null,
|
|
4406
|
+
/**
|
|
4407
|
+
* 🔴 A `<br>` IS A SPACE TO A READER AND NOTHING TO `textContent`, and the derive lane reads
|
|
4408
|
+
* the DOM. `<h1>Launch your site<br /><span>fast.</span></h1>` is one INLINE richtext field
|
|
4409
|
+
* whose value is `Launch your sitefast.` — no space, because no text node holds one — while
|
|
4410
|
+
* this prefilter's only reading inserted one, so the file was never opened and every split
|
|
4411
|
+
* heading on every imported site came back NOT_IN_SOURCE. Both readings are searched: this
|
|
4412
|
+
* decides which FILES to open, and `collectSites` still has to match an element exactly.
|
|
4413
|
+
*/
|
|
4414
|
+
tight: options?.stripTags ? flat(stripTags(readable, "")) : null
|
|
4384
4415
|
};
|
|
4385
4416
|
});
|
|
4386
4417
|
const located = [];
|
|
@@ -4392,7 +4423,9 @@ function locate(brief, sources, options) {
|
|
|
4392
4423
|
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
4393
4424
|
continue;
|
|
4394
4425
|
}
|
|
4395
|
-
const files = flattened.filter(
|
|
4426
|
+
const files = flattened.filter(
|
|
4427
|
+
(f) => f.content.includes(original) || f.text !== null && f.text.includes(original) || f.tight !== null && f.tight.includes(original)
|
|
4428
|
+
).map((f) => f.path);
|
|
4396
4429
|
if (files.length === 0) {
|
|
4397
4430
|
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
4398
4431
|
continue;
|
|
@@ -5091,7 +5124,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
5091
5124
|
addDeclaration(site.attrInsertAt, declaration(rewrite));
|
|
5092
5125
|
continue;
|
|
5093
5126
|
}
|
|
5094
|
-
if (kind
|
|
5127
|
+
if (ownsSubtree(kind, site, rewrite.inline)) {
|
|
5095
5128
|
if (!isIntrinsic(site.tag)) continue;
|
|
5096
5129
|
if (rule.richtext && site.inner) {
|
|
5097
5130
|
source.remove(site.inner.start, site.inner.end);
|
|
@@ -5160,6 +5193,22 @@ function importLines(imports) {
|
|
|
5160
5193
|
|
|
5161
5194
|
// src/receipt.ts
|
|
5162
5195
|
var CANVAS_REASONS = /* @__PURE__ */ new Set(["CANVAS_BRIDGE_MANUAL", "SSR_DRAFT_ROUTE"]);
|
|
5196
|
+
var FormInvariantError = class extends Error {
|
|
5197
|
+
code = "FORM_INVARIANT";
|
|
5198
|
+
constructor(message) {
|
|
5199
|
+
super(message);
|
|
5200
|
+
this.name = "FormInvariantError";
|
|
5201
|
+
}
|
|
5202
|
+
};
|
|
5203
|
+
function assertForms(total, forms) {
|
|
5204
|
+
const sum = forms.wired + forms.alreadyWired + forms.pending.length;
|
|
5205
|
+
if (sum !== total) {
|
|
5206
|
+
throw new FormInvariantError(
|
|
5207
|
+
`The receipt does not account for every form: ${total} listed but ${forms.wired} wired + ${forms.alreadyWired} already wired + ${forms.pending.length} pending = ${sum}.`
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
return forms;
|
|
5211
|
+
}
|
|
5163
5212
|
var ReceiptInvariantError = class extends Error {
|
|
5164
5213
|
code = "RECEIPT_INVARIANT";
|
|
5165
5214
|
constructor(message) {
|
|
@@ -6011,6 +6060,17 @@ function readBrief(value) {
|
|
|
6011
6060
|
}
|
|
6012
6061
|
}
|
|
6013
6062
|
}
|
|
6063
|
+
for (const [at, entry] of check.array(brief.forms ?? [], `the brief's "forms"`).entries()) {
|
|
6064
|
+
const where = `brief.forms[${at}]`;
|
|
6065
|
+
const form = check.record(entry, where);
|
|
6066
|
+
check.segment(form.id, `${where}.id`);
|
|
6067
|
+
check.string(form.name, `${where}.name`);
|
|
6068
|
+
check.string(form.submitUrl, `${where}.submitUrl`);
|
|
6069
|
+
check.optionalString(form.successMessage, `${where}.successMessage`);
|
|
6070
|
+
for (const [n, held] of check.array(form.fields, `${where}.fields`).entries()) {
|
|
6071
|
+
check.segment(check.record(held, `${where}.fields[${n}]`).key, `${where}.fields[${n}].key`);
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6014
6074
|
return brief;
|
|
6015
6075
|
}
|
|
6016
6076
|
function readPlan(value) {
|
|
@@ -6057,6 +6117,236 @@ function readPlan(value) {
|
|
|
6057
6117
|
return plan2;
|
|
6058
6118
|
}
|
|
6059
6119
|
|
|
6120
|
+
// src/forms.ts
|
|
6121
|
+
import MagicString4 from "magic-string";
|
|
6122
|
+
var WIRABLE = ["html", "astro", "jsx"];
|
|
6123
|
+
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6124
|
+
var FORM_SCRIPT = `<script>
|
|
6125
|
+
document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
6126
|
+
f.addEventListener('submit',function(e){
|
|
6127
|
+
e.preventDefault();
|
|
6128
|
+
var msg=f.querySelector('.bcms-form-msg');
|
|
6129
|
+
var data={};
|
|
6130
|
+
// A checkboxes group posts one entry per checked box under the SAME name; the old
|
|
6131
|
+
// one-at-a-time assignment kept only the last, so "pick many" delivered one answer.
|
|
6132
|
+
// One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a
|
|
6133
|
+
// lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts
|
|
6134
|
+
// both an array and a comma string.
|
|
6135
|
+
new FormData(f).forEach(function(v,k){
|
|
6136
|
+
if(k==='cf-turnstile-response')return;
|
|
6137
|
+
if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
|
|
6138
|
+
else data[k]=v;
|
|
6139
|
+
});
|
|
6140
|
+
var body={data:data};
|
|
6141
|
+
var tok=f.querySelector('[name="cf-turnstile-response"]');
|
|
6142
|
+
if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
|
|
6143
|
+
fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
6144
|
+
.then(function(r){return r.ok;})
|
|
6145
|
+
.then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})
|
|
6146
|
+
.catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});
|
|
6147
|
+
});
|
|
6148
|
+
});
|
|
6149
|
+
</script>`;
|
|
6150
|
+
var SCRIPT_BODY = FORM_SCRIPT.slice("<script>".length, -"</script>".length);
|
|
6151
|
+
function scriptFor(dialect) {
|
|
6152
|
+
if (dialect === "jsx") return `<script dangerouslySetInnerHTML={{ __html: \`${SCRIPT_BODY}\` }} />`;
|
|
6153
|
+
if (dialect === "astro") return `<script is:inline>${SCRIPT_BODY}</script>`;
|
|
6154
|
+
return FORM_SCRIPT;
|
|
6155
|
+
}
|
|
6156
|
+
var messageFor = (dialect) => dialect === "jsx" ? `<p className="bcms-form-msg" hidden></p>` : `<p class="bcms-form-msg" hidden></p>`;
|
|
6157
|
+
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
6158
|
+
var attrOf = (el, name) => el.attrs.find((a) => a.name.toLowerCase() === name);
|
|
6159
|
+
function* elementsIn(nodes) {
|
|
6160
|
+
for (const node of nodes) {
|
|
6161
|
+
if (node.type === "expr") {
|
|
6162
|
+
yield* elementsIn(node.children);
|
|
6163
|
+
continue;
|
|
6164
|
+
}
|
|
6165
|
+
if (node.type !== "element") continue;
|
|
6166
|
+
yield node;
|
|
6167
|
+
yield* elementsIn(node.children);
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
var childElements = (node) => node.children.flatMap(
|
|
6171
|
+
(child) => child.type === "element" ? [child] : child.type === "expr" ? childElements(child) : []
|
|
6172
|
+
);
|
|
6173
|
+
function candidateOf(file, dialect, form) {
|
|
6174
|
+
const controls = /* @__PURE__ */ new Map();
|
|
6175
|
+
const wrapper = /* @__PURE__ */ new Map();
|
|
6176
|
+
const all = [];
|
|
6177
|
+
const walk = (parent) => {
|
|
6178
|
+
const own = [];
|
|
6179
|
+
for (const child of childElements(parent)) {
|
|
6180
|
+
if (CONTROLS.has(child.tag.toLowerCase())) {
|
|
6181
|
+
own.push(child);
|
|
6182
|
+
all.push(child);
|
|
6183
|
+
}
|
|
6184
|
+
walk(child);
|
|
6185
|
+
}
|
|
6186
|
+
if (own.length === 1 && parent !== form) wrapper.set(own[0], parent);
|
|
6187
|
+
};
|
|
6188
|
+
walk(form);
|
|
6189
|
+
for (const control of all) {
|
|
6190
|
+
const name = attrOf(control, "name")?.value;
|
|
6191
|
+
if (name && !controls.has(name)) controls.set(name, control);
|
|
6192
|
+
}
|
|
6193
|
+
for (const control of all) {
|
|
6194
|
+
const id = attrOf(control, "id")?.value;
|
|
6195
|
+
if (id && !controls.has(id)) controls.set(id, control);
|
|
6196
|
+
}
|
|
6197
|
+
return { file, dialect, el: form, controls, wrapper };
|
|
6198
|
+
}
|
|
6199
|
+
var hasMessage = (form) => [...elementsIn([form])].some((el) => (attrOf(el, "class") ?? attrOf(el, "classname"))?.value.includes("bcms-form-msg") ?? false);
|
|
6200
|
+
function wire(source, candidate, form) {
|
|
6201
|
+
const { el, dialect } = candidate;
|
|
6202
|
+
const inserts = [];
|
|
6203
|
+
const set = (name, value) => {
|
|
6204
|
+
const attr = attrOf(el, name);
|
|
6205
|
+
if (!attr) inserts.push(`${name}="${escapeAttr(value)}"`);
|
|
6206
|
+
else if (attr.valueRange) source.overwrite(attr.valueRange.start, attr.valueRange.end, escapeAttr(value));
|
|
6207
|
+
else source.overwrite(attr.range.start, attr.range.end, `${name}="${escapeAttr(value)}"`);
|
|
6208
|
+
};
|
|
6209
|
+
set("action", form.submitUrl);
|
|
6210
|
+
set("method", "post");
|
|
6211
|
+
inserts.push(`data-bcms-form="${escapeAttr(form.id)}"`);
|
|
6212
|
+
if (form.successMessage) inserts.push(`data-bcms-form-success="${escapeAttr(form.successMessage)}"`);
|
|
6213
|
+
source.appendLeft(el.attrInsertAt, ` ${inserts.join(" ")}`);
|
|
6214
|
+
for (const field of form.fields) {
|
|
6215
|
+
const control = candidate.controls.get(field.key);
|
|
6216
|
+
const host = candidate.wrapper.get(control) ?? control;
|
|
6217
|
+
source.appendLeft(host.attrInsertAt, ` data-bcms-form-field="${escapeAttr(field.key)}"`);
|
|
6218
|
+
const name = attrOf(control, "name");
|
|
6219
|
+
if (!name) source.appendLeft(control.attrInsertAt, ` name="${escapeAttr(field.key)}"`);
|
|
6220
|
+
else if (!name.expression && name.value !== field.key && name.valueRange) {
|
|
6221
|
+
source.overwrite(name.valueRange.start, name.valueRange.end, escapeAttr(field.key));
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
if (el.inner && !hasMessage(el)) {
|
|
6225
|
+
const tail = /\s*$/.exec(source.original.slice(el.inner.start, el.inner.end))[0];
|
|
6226
|
+
source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
async function wireForms(briefIn, sources) {
|
|
6230
|
+
const brief = readBrief(briefIn);
|
|
6231
|
+
const forms = brief.forms ?? [];
|
|
6232
|
+
const candidates = [];
|
|
6233
|
+
const unreadable = [];
|
|
6234
|
+
for (const source of sources) {
|
|
6235
|
+
if (!/<form[\s/>]/i.test(source.content)) continue;
|
|
6236
|
+
const dialect = dialectOf(source.path, source.content);
|
|
6237
|
+
if (!dialect || !WIRABLE.includes(dialect)) {
|
|
6238
|
+
unreadable.push({
|
|
6239
|
+
file: source.path,
|
|
6240
|
+
reason: "DIALECT_UNSUPPORTED",
|
|
6241
|
+
message: `${source.path} is ${dialect ?? "not a template"}.`
|
|
6242
|
+
});
|
|
6243
|
+
continue;
|
|
6244
|
+
}
|
|
6245
|
+
const parsed = await parseFile4(dialect, source.content);
|
|
6246
|
+
if (parsed.error) {
|
|
6247
|
+
unreadable.push({
|
|
6248
|
+
file: source.path,
|
|
6249
|
+
reason: parsed.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR",
|
|
6250
|
+
message: parsed.error.message
|
|
6251
|
+
});
|
|
6252
|
+
continue;
|
|
6253
|
+
}
|
|
6254
|
+
for (const el of elementsIn(parsed.roots)) {
|
|
6255
|
+
if (el.tag.toLowerCase() === "form") candidates.push(candidateOf(source.path, dialect, el));
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
|
|
6259
|
+
const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
|
|
6260
|
+
const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
|
|
6261
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
6262
|
+
for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
|
|
6263
|
+
const matched = [];
|
|
6264
|
+
const pending = [];
|
|
6265
|
+
const onSite = [];
|
|
6266
|
+
let alreadyWired = 0;
|
|
6267
|
+
const pend2 = (f, reason, file, message) => void pending.push({ id: f.id, name: f.name, reason, ...file ? { file } : {}, ...message ? { message } : {} });
|
|
6268
|
+
for (const form of forms) {
|
|
6269
|
+
const list = options.get(form.id) ?? [];
|
|
6270
|
+
if (list.length > 1) {
|
|
6271
|
+
pend2(form, "FORM_AMBIGUOUS", void 0, `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.`);
|
|
6272
|
+
continue;
|
|
6273
|
+
}
|
|
6274
|
+
if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
|
|
6275
|
+
pend2(form, "FORM_AMBIGUOUS", list[0].file, "more than one CMS form's fields are posted by this <form>.");
|
|
6276
|
+
continue;
|
|
6277
|
+
}
|
|
6278
|
+
if (list.length === 0) {
|
|
6279
|
+
const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
|
|
6280
|
+
if (best) {
|
|
6281
|
+
const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
|
|
6282
|
+
pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
|
|
6283
|
+
} else if (unreadable[0]) {
|
|
6284
|
+
pend2(form, unreadable[0].reason, unreadable[0].file, unreadable[0].message);
|
|
6285
|
+
} else {
|
|
6286
|
+
pend2(form, "FORM_NOT_IN_SOURCE", void 0, `no <form> posts ${form.fields.map((f) => f.key).join(", ") || "any field of this form"}.`);
|
|
6287
|
+
}
|
|
6288
|
+
continue;
|
|
6289
|
+
}
|
|
6290
|
+
const candidate = list[0];
|
|
6291
|
+
if (attrOf(candidate.el, "action")?.expression) {
|
|
6292
|
+
pend2(form, "ACTION_IS_EXPRESSION", candidate.file, "its action is an expression, so this run will not replace it.");
|
|
6293
|
+
continue;
|
|
6294
|
+
}
|
|
6295
|
+
const misnamed = form.fields.find((field) => {
|
|
6296
|
+
const attr = attrOf(candidate.controls.get(field.key), "name");
|
|
6297
|
+
return !!attr && attr.value !== field.key && (attr.expression || !attr.valueRange);
|
|
6298
|
+
});
|
|
6299
|
+
if (misnamed) {
|
|
6300
|
+
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.`);
|
|
6301
|
+
continue;
|
|
6302
|
+
}
|
|
6303
|
+
if (attrOf(candidate.el, "data-bcms-form")) {
|
|
6304
|
+
alreadyWired++;
|
|
6305
|
+
onSite.push(form);
|
|
6306
|
+
continue;
|
|
6307
|
+
}
|
|
6308
|
+
matched.push({ form, candidate });
|
|
6309
|
+
onSite.push(form);
|
|
6310
|
+
}
|
|
6311
|
+
const contents = new Map(sources.map((s) => [s.path, s.content]));
|
|
6312
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6313
|
+
for (const m of matched) byFile.set(m.candidate.file, [...byFile.get(m.candidate.file) ?? [], m]);
|
|
6314
|
+
const files = [];
|
|
6315
|
+
for (const [path, list] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6316
|
+
const content = contents.get(path);
|
|
6317
|
+
const source = new MagicString4(content);
|
|
6318
|
+
for (const { form, candidate } of list) wire(source, candidate, form);
|
|
6319
|
+
if (!content.includes("form[data-bcms-form]")) {
|
|
6320
|
+
const last = list.reduce((a, b) => b.candidate.el.end > a.candidate.el.end ? b : a);
|
|
6321
|
+
const indent = /[^\S\n]*$/.exec(content.slice(0, last.candidate.el.start))[0];
|
|
6322
|
+
source.appendRight(last.candidate.el.end, `
|
|
6323
|
+
${indent}${scriptFor(last.candidate.dialect)}`);
|
|
6324
|
+
}
|
|
6325
|
+
files.push({ path, operation: "modify", content: source.toString() });
|
|
6326
|
+
}
|
|
6327
|
+
const notes = onSite.filter((f) => f.status === "draft").map((f) => `publish form ${f.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
|
|
6328
|
+
return {
|
|
6329
|
+
files,
|
|
6330
|
+
receipt: {
|
|
6331
|
+
briefDigest: briefDigest(brief),
|
|
6332
|
+
// This lane binds no brief PATH, and says so rather than leaving the arithmetic to a reader:
|
|
6333
|
+
// a receipt claiming zero of zero paths is exactly what a forms-only run did.
|
|
6334
|
+
paths: { declared: 0, rewritten: 0, alreadyDeclared: 0, pending: [] },
|
|
6335
|
+
occurrences: { located: 0, rewritten: 0 },
|
|
6336
|
+
files: [],
|
|
6337
|
+
helpers: [],
|
|
6338
|
+
helperUpgraded: false,
|
|
6339
|
+
dynamicBindings: [],
|
|
6340
|
+
forms: assertForms(forms.length, {
|
|
6341
|
+
wired: matched.length,
|
|
6342
|
+
alreadyWired,
|
|
6343
|
+
pending,
|
|
6344
|
+
...notes.length > 0 ? { notes } : {}
|
|
6345
|
+
})
|
|
6346
|
+
}
|
|
6347
|
+
};
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6060
6350
|
// src/scan.ts
|
|
6061
6351
|
var SKIP_DIRS = [
|
|
6062
6352
|
"node_modules/",
|
|
@@ -6120,6 +6410,66 @@ function isSourceCandidate(path) {
|
|
|
6120
6410
|
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
6121
6411
|
}
|
|
6122
6412
|
|
|
6413
|
+
// src/xray.ts
|
|
6414
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["svg", "iframe", "head", "title"]);
|
|
6415
|
+
var CONTEXT_TAGS = /* @__PURE__ */ new Set([
|
|
6416
|
+
"nav",
|
|
6417
|
+
"header",
|
|
6418
|
+
"footer",
|
|
6419
|
+
"main",
|
|
6420
|
+
"aside",
|
|
6421
|
+
"form",
|
|
6422
|
+
"button",
|
|
6423
|
+
"a",
|
|
6424
|
+
"ul",
|
|
6425
|
+
"ol",
|
|
6426
|
+
"table",
|
|
6427
|
+
"dialog",
|
|
6428
|
+
"details",
|
|
6429
|
+
"figure",
|
|
6430
|
+
"blockquote"
|
|
6431
|
+
]);
|
|
6432
|
+
var BOUND_ATTRS = ["data-bcms-field", "data-bcms-layout-field", "data-bcms-path", "data-bcms-block"];
|
|
6433
|
+
var isDecorativeGlyph = (text) => text.length <= 2 || /^\d{1,2}\s*[/.)\-–]?$/.test(text) || !new RegExp("\\p{L}", "u").test(text) || /^made with\b/i.test(text) || /^skip to\b/i.test(text);
|
|
6434
|
+
var MAX_BUCKETS = 12;
|
|
6435
|
+
var flat2 = (value) => value.replace(/\s+/g, " ").trim();
|
|
6436
|
+
function measureUnaddressable(html) {
|
|
6437
|
+
const parsed = parse2(html);
|
|
6438
|
+
let visible = 0;
|
|
6439
|
+
let unaddressable = 0;
|
|
6440
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6441
|
+
const visit = (node, tag, context, bound) => {
|
|
6442
|
+
if (node.type === "text") {
|
|
6443
|
+
const text = flat2(node.value);
|
|
6444
|
+
if (!text || isDecorativeGlyph(text)) return;
|
|
6445
|
+
visible += text.length;
|
|
6446
|
+
if (bound) return;
|
|
6447
|
+
unaddressable += text.length;
|
|
6448
|
+
const key = `${tag} @ ${context}`;
|
|
6449
|
+
const held = buckets.get(key) ?? { tag, context, chars: 0, nodes: 0, samples: [] };
|
|
6450
|
+
held.chars += text.length;
|
|
6451
|
+
held.nodes += 1;
|
|
6452
|
+
if (held.samples.length < 2) held.samples.push(text.slice(0, 50));
|
|
6453
|
+
buckets.set(key, held);
|
|
6454
|
+
return;
|
|
6455
|
+
}
|
|
6456
|
+
if (node.type !== "element" || node.raw || SKIP_TAGS.has(node.tag)) return;
|
|
6457
|
+
const attrs = new Map(node.attrs.map((a) => [a.name, a.value]));
|
|
6458
|
+
if (attrs.has("hidden") || attrs.get("aria-hidden") === "true" || /display\s*:\s*none/.test(attrs.get("style") ?? "")) {
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
6461
|
+
const owned = bound || BOUND_ATTRS.some((name) => attrs.has(name));
|
|
6462
|
+
const chain = CONTEXT_TAGS.has(node.tag) ? context === "body" ? node.tag : `${context}>${node.tag}` : context;
|
|
6463
|
+
for (const child of node.children) visit(child, node.tag, chain, owned);
|
|
6464
|
+
};
|
|
6465
|
+
for (const root of parsed.roots) visit(root, "body", "body", false);
|
|
6466
|
+
return {
|
|
6467
|
+
visible,
|
|
6468
|
+
unaddressable,
|
|
6469
|
+
buckets: [...buckets.values()].sort((a, b) => b.chars - a.chars).slice(0, MAX_BUCKETS)
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6123
6473
|
// src/declared.ts
|
|
6124
6474
|
async function readDeclarations(file, content) {
|
|
6125
6475
|
const dialect = dialectOf(file, content);
|
|
@@ -6425,7 +6775,7 @@ function moduleScope(parsed, content) {
|
|
|
6425
6775
|
}
|
|
6426
6776
|
|
|
6427
6777
|
// src/componentize.ts
|
|
6428
|
-
import
|
|
6778
|
+
import MagicString5 from "magic-string";
|
|
6429
6779
|
var SectionInvariantError = class extends Error {
|
|
6430
6780
|
code = "SECTION_INVARIANT";
|
|
6431
6781
|
constructor(message) {
|
|
@@ -7351,7 +7701,7 @@ export default function ${resolved.name}({ blockId, bind, overrides, page }: Bcm
|
|
|
7351
7701
|
` };
|
|
7352
7702
|
}
|
|
7353
7703
|
function sectionMarkup(content, root, fieldName) {
|
|
7354
|
-
const slice = new
|
|
7704
|
+
const slice = new MagicString5(content.slice(root.start, root.end));
|
|
7355
7705
|
const at = (offset) => offset - root.start;
|
|
7356
7706
|
const stamps = root.attrs.filter((attr) => attr.name === "data-bcms-block");
|
|
7357
7707
|
if (stamps.length === 0) slice.appendLeft(at(root.attrInsertAt), " data-bcms-block={blockId}");
|
|
@@ -7550,7 +7900,7 @@ async function migrateSections(plan2, contents, written, fileSet, aliases) {
|
|
|
7550
7900
|
);
|
|
7551
7901
|
continue;
|
|
7552
7902
|
}
|
|
7553
|
-
const source = new
|
|
7903
|
+
const source = new MagicString5(content);
|
|
7554
7904
|
const { bound, duplicates } = wrapFieldAttrs(source, content, elements, (at) => at, "bcmsField");
|
|
7555
7905
|
if (duplicates.length > 0) {
|
|
7556
7906
|
refused.set(refusalKey(dialect, slug), `${entry.path} has ${duplicates.join(", ")} carrying more than one data-bcms-field, so there is no single address to rewrite.`);
|
|
@@ -7827,7 +8177,7 @@ export default function Sections({ page }: { page: string }) {
|
|
|
7827
8177
|
function splicePageRun(file, content, dialect, parsed, placeable, pageSlug) {
|
|
7828
8178
|
const names = allocateNames(topLevelBindings(dialect, parsed, content), [{ key: "Sections", name: "Sections" }]);
|
|
7829
8179
|
const local = names.get("Sections");
|
|
7830
|
-
const source = new
|
|
8180
|
+
const source = new MagicString5(content);
|
|
7831
8181
|
const first = placeable[0].root;
|
|
7832
8182
|
const last = placeable[placeable.length - 1].root;
|
|
7833
8183
|
source.overwrite(first.start, last.end, `<${local} page="${pageSlug}" />`);
|
|
@@ -7847,7 +8197,7 @@ function splicePageInline(file, content, dialect, parsed, placeable, pageSlug, w
|
|
|
7847
8197
|
{ key: "pageSnapshot", name: "pageSnapshot" }
|
|
7848
8198
|
]);
|
|
7849
8199
|
const snapshot = names.get("pageSnapshot");
|
|
7850
|
-
const source = new
|
|
8200
|
+
const source = new MagicString5(content);
|
|
7851
8201
|
for (const resolved of placeable) {
|
|
7852
8202
|
const component = written.get(resolved.slug);
|
|
7853
8203
|
const tag = component.member ? `${names.get(resolved.slug)}.${component.member}` : names.get(resolved.slug);
|
|
@@ -8008,7 +8358,8 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8008
8358
|
kind: p.kind,
|
|
8009
8359
|
fallback: p.original ?? "",
|
|
8010
8360
|
literal: norm(p.original ?? ""),
|
|
8011
|
-
locator: p.locator
|
|
8361
|
+
locator: p.locator,
|
|
8362
|
+
...p.inline ? { inline: true } : {}
|
|
8012
8363
|
});
|
|
8013
8364
|
}
|
|
8014
8365
|
}
|
|
@@ -8233,8 +8584,18 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8233
8584
|
declaredRow(target, witness !== null, witness ?? byHand);
|
|
8234
8585
|
} else rewritten++;
|
|
8235
8586
|
}
|
|
8587
|
+
const templated = dynamicBindings.filter((d) => d.prop === null);
|
|
8588
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
8589
|
+
const row = pending[i];
|
|
8590
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8591
|
+
if (row.reason !== "NO_ORIGINAL" || !target || target.fallback !== "") continue;
|
|
8592
|
+
const shape = shapeOfPath(target.path);
|
|
8593
|
+
if (!templated.some((d) => d.path === shape && attributable(target, d.file))) continue;
|
|
8594
|
+
pending.splice(i, 1);
|
|
8595
|
+
rewritten++;
|
|
8596
|
+
}
|
|
8236
8597
|
for (const [file, splices] of [...componentEdits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
8237
|
-
const source = new
|
|
8598
|
+
const source = new MagicString6(contents.get(file));
|
|
8238
8599
|
const seen = /* @__PURE__ */ new Set();
|
|
8239
8600
|
for (const splice of splices) {
|
|
8240
8601
|
const id = `${splice.start}:${splice.end}:${splice.text}`;
|
|
@@ -8545,6 +8906,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8545
8906
|
}
|
|
8546
8907
|
let refusal = null;
|
|
8547
8908
|
for (const site of fresh) {
|
|
8909
|
+
if (target.inline && site.where === "text" && !ownsSubtree(target.kind, site, true)) {
|
|
8910
|
+
refusal = "SUBTREE_NOT_HTML";
|
|
8911
|
+
continue;
|
|
8912
|
+
}
|
|
8548
8913
|
let bindings;
|
|
8549
8914
|
let bindingsMerge;
|
|
8550
8915
|
if (site.where === "prop" && site.attr) {
|
|
@@ -8579,7 +8944,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8579
8944
|
// 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
|
|
8580
8945
|
// a mixed element gets a wrapper of its own, so taking the element's inner range as its
|
|
8581
8946
|
// fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
|
|
8582
|
-
|
|
8947
|
+
...target.inline ? { inline: true } : {},
|
|
8948
|
+
fallback: site.inner && ownsSubtree(target.kind, site, target.inline) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
|
|
8583
8949
|
snapshot: snapshotName(target.slug),
|
|
8584
8950
|
slug: target.slug,
|
|
8585
8951
|
scope: target.scope,
|
|
@@ -8942,6 +9308,8 @@ export {
|
|
|
8942
9308
|
CONTENT_DIR,
|
|
8943
9309
|
ConvertError,
|
|
8944
9310
|
DIALECT_RULES,
|
|
9311
|
+
FORM_SCRIPT,
|
|
9312
|
+
FormInvariantError,
|
|
8945
9313
|
HELPER_PATH,
|
|
8946
9314
|
HELPER_VERSION,
|
|
8947
9315
|
PARSE_FILE,
|
|
@@ -8957,6 +9325,7 @@ export {
|
|
|
8957
9325
|
SectionInvariantError,
|
|
8958
9326
|
ValidationError,
|
|
8959
9327
|
aliasesFrom,
|
|
9328
|
+
assertForms,
|
|
8960
9329
|
assertReceipt,
|
|
8961
9330
|
assertSections,
|
|
8962
9331
|
assetCandidates,
|
|
@@ -8980,10 +9349,12 @@ export {
|
|
|
8980
9349
|
flat,
|
|
8981
9350
|
helperSource,
|
|
8982
9351
|
importsIn,
|
|
9352
|
+
isDecorativeGlyph,
|
|
8983
9353
|
isDynamicRoute,
|
|
8984
9354
|
isKnownHelper,
|
|
8985
9355
|
isSourceCandidate,
|
|
8986
9356
|
locate,
|
|
9357
|
+
measureUnaddressable,
|
|
8987
9358
|
overlapping,
|
|
8988
9359
|
pageFilesFor,
|
|
8989
9360
|
parseFile4 as parseFile,
|
|
@@ -8999,6 +9370,7 @@ export {
|
|
|
8999
9370
|
routeOfFile,
|
|
9000
9371
|
stripTags,
|
|
9001
9372
|
unreadableConfigs,
|
|
9002
|
-
walkAst
|
|
9373
|
+
walkAst,
|
|
9374
|
+
wireForms
|
|
9003
9375
|
};
|
|
9004
9376
|
//# sourceMappingURL=index.js.map
|