@bettercms-ai/convert 0.26.0 → 0.26.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
@@ -1203,6 +1203,8 @@ interface ImageTarget {
1203
1203
  /** The brief's `original` — the BUILT url. */
1204
1204
  original: string;
1205
1205
  slug: string;
1206
+ /** The file already declares this path: only a missing READ is added, and nothing is refused. */
1207
+ declared?: boolean;
1206
1208
  }
1207
1209
  interface ImageSplice {
1208
1210
  start: number;
package/dist/index.js CHANGED
@@ -4086,6 +4086,13 @@ var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".svg
4086
4086
  var ASSET_COMPONENTS = /* @__PURE__ */ new Set(["Image", "Picture"]);
4087
4087
  var ASSET_MODULE = "astro:assets";
4088
4088
  function baseName(url) {
4089
+ const inner = /[?&](?:href|url)=([^&#]+)/.exec(url)?.[1];
4090
+ if (inner) {
4091
+ try {
4092
+ url = decodeURIComponent(inner);
4093
+ } catch {
4094
+ }
4095
+ }
4089
4096
  const file = url.split(/[?#]/)[0].split("/").pop() ?? "";
4090
4097
  const base = file.split(".")[0] ?? "";
4091
4098
  return base === "" ? null : base.toLowerCase();
@@ -4111,6 +4118,29 @@ function passedTo(content, parsed, binding) {
4111
4118
  for (const root of parsed.roots) visit(root);
4112
4119
  return out;
4113
4120
  }
4121
+ function declaredAt(content, parsed, binding, path) {
4122
+ const out = [];
4123
+ const names = [`"${path}"`, `'${path}'`];
4124
+ const visit = (node) => {
4125
+ if (node.type === "expr") {
4126
+ for (const child of node.children) visit(child);
4127
+ return;
4128
+ }
4129
+ if (node.type !== "element" || node.raw) return;
4130
+ const bindings = node.attrs.find((a) => a.name === "bcmsBindings");
4131
+ const declared = bindings?.expressionRange ? content.slice(bindings.expressionRange.start, bindings.expressionRange.end) : "";
4132
+ if (/^[A-Z]/.test(node.tag) && !ASSET_COMPONENTS.has(node.tag) && names.some((name) => declared.includes(name))) {
4133
+ for (const attr of node.attrs) {
4134
+ if (!attr.expressionRange) continue;
4135
+ const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
4136
+ if (inner === binding) out.push({ node, prop: attr.name, attr });
4137
+ }
4138
+ }
4139
+ for (const child of node.children) visit(child);
4140
+ };
4141
+ for (const root of parsed.roots) visit(root);
4142
+ return out;
4143
+ }
4114
4144
  function assetCandidates(parsed, dialect, original) {
4115
4145
  const wanted = baseName(original);
4116
4146
  if (wanted === null || parsed.error) return [];
@@ -4158,6 +4188,18 @@ async function bindImages(ctx, targets) {
4158
4188
  const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
4159
4189
  const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
4160
4190
  const args = (b) => [ctx.snapshotName(target.slug), JSON.stringify(target.path), b, ...ctx.slug ? [ctx.slug] : []];
4191
+ if (free.length === 0 && binding !== null) {
4192
+ const declared = declaredAt(ctx.content, ctx.parsed, binding, target.path).filter((c) => !claimed.has(c.attr.range.start));
4193
+ if (declared.length === 1) {
4194
+ const call = declared[0];
4195
+ claimed.add(call.attr.range.start);
4196
+ out.splices.push({ start: call.attr.range.start, end: call.attr.range.end, text: rule.attr(call.prop, `${ctx.image}(${args(binding).join(", ")})`) });
4197
+ out.slugs.push(target.slug);
4198
+ out.bound.push(target.key);
4199
+ continue;
4200
+ }
4201
+ }
4202
+ if (target.declared) continue;
4161
4203
  if (free.length === 0 && binding !== null && ctx.drill) {
4162
4204
  const calls = passedTo(ctx.content, ctx.parsed, binding).filter((c) => !claimed.has(c.node.attrInsertAt));
4163
4205
  const call = calls.length === 1 ? calls[0] : null;
@@ -10209,10 +10251,13 @@ async function convertSources(briefIn, sources, options) {
10209
10251
  async function convertFile(file, content, targets, parsed, declared, llmFallback, drill) {
10210
10252
  const outcomes = /* @__PURE__ */ new Map();
10211
10253
  const pendingTargets = [];
10254
+ let unreadImages = false;
10212
10255
  const dialect = dialectOf(file, content);
10213
10256
  for (const target of targets) {
10214
10257
  if (parsed && dialect && convertedHere(content, parsed, dialect, target, file)) {
10215
10258
  outcomes.set(target.key, "declared");
10259
+ if (target.kind === "image" && !content.includes("bcmsImage(")) unreadImages = true;
10260
+ else if (target.kind === "image" && !new RegExp(`bcmsImage\\([^)]*["']${target.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content)) unreadImages = true;
10216
10261
  } else pendingTargets.push(target);
10217
10262
  }
10218
10263
  const empty = {
@@ -10227,7 +10272,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
10227
10272
  snapshots: [],
10228
10273
  notes: []
10229
10274
  };
10230
- if (pendingTargets.length === 0) return empty;
10275
+ if (pendingTargets.length === 0 && !unreadImages) return empty;
10231
10276
  if (!dialect) {
10232
10277
  if (isTolerantDialect(file)) {
10233
10278
  const already = declaredByScan(content);
@@ -10428,15 +10473,15 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
10428
10473
  continue;
10429
10474
  }
10430
10475
  const declares = (d) => d.scope === target.scope && (d.path === target.path || d.path === shapeOfPath(target.path));
10431
- const declaredAt = new Set(declarations.filter(declares).map((d) => d.at));
10476
+ const declaredAt2 = new Set(declarations.filter(declares).map((d) => d.at));
10432
10477
  const claimedAt = new Set(declarations.filter((d) => !declares(d) && d.via === "field").map((d) => d.at));
10433
10478
  const writesField = (site) => site.where !== "attr" || target.kind === "image";
10434
10479
  const fresh = eligible.filter(
10435
- (site) => !declaredAt.has(site.attrInsertAt) && !(writesField(site) && claimedAt.has(site.attrInsertAt))
10480
+ (site) => !declaredAt2.has(site.attrInsertAt) && !(writesField(site) && claimedAt.has(site.attrInsertAt))
10436
10481
  );
10437
10482
  located += eligible.length;
10438
10483
  const blocked = eligible.filter(
10439
- (site) => !declaredAt.has(site.attrInsertAt) && writesField(site) && claimedAt.has(site.attrInsertAt)
10484
+ (site) => !declaredAt2.has(site.attrInsertAt) && writesField(site) && claimedAt.has(site.attrInsertAt)
10440
10485
  );
10441
10486
  if (blocked.length > 0) {
10442
10487
  outcomes.set(target.key, "AMBIGUOUS_LITERAL");
@@ -10445,7 +10490,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
10445
10490
  if (fresh.length === 0) {
10446
10491
  outcomes.set(
10447
10492
  target.key,
10448
- eligible.some((site) => declaredAt.has(site.attrInsertAt)) ? "declared" : "AMBIGUOUS_LITERAL"
10493
+ eligible.some((site) => declaredAt2.has(site.attrInsertAt)) ? "declared" : "AMBIGUOUS_LITERAL"
10449
10494
  );
10450
10495
  continue;
10451
10496
  }
@@ -10631,8 +10676,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
10631
10676
  },
10632
10677
  targets.filter((t) => {
10633
10678
  const outcome = outcomes.get(t.key);
10634
- return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
10635
- }).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug }))
10679
+ return t.kind === "image" && t.scope === "page" && (outcome === void 0 || outcome === "declared" || UNPLACED.has(outcome));
10680
+ }).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug, declared: outcomes.get(t.key) === "declared" }))
10636
10681
  );
10637
10682
  for (const key of images.bound) outcomes.set(key, "rewritten");
10638
10683
  for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);