@fenfill/mcp 0.2.0 → 0.2.2
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/README.md +86 -35
- package/THIRD_PARTY_NOTICES +1 -1
- package/dist/index.js +916 -210
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -135572,6 +135572,37 @@ function layoutMultiline({
|
|
|
135572
135572
|
}
|
|
135573
135573
|
|
|
135574
135574
|
// ../../app/t/[template_id]/stampPdf.ts
|
|
135575
|
+
async function embedStampFaces(pdfDoc, fontkit, fonts) {
|
|
135576
|
+
const interFont = await pdfDoc.embedFont(fonts.inter, { subset: true });
|
|
135577
|
+
const dejaFont = await pdfDoc.embedFont(fonts.deja, { subset: true });
|
|
135578
|
+
const interKit = fontkit.create(new Uint8Array(fonts.inter));
|
|
135579
|
+
const covers = (cp) => {
|
|
135580
|
+
try {
|
|
135581
|
+
return interKit.hasGlyphForCodePoint(cp);
|
|
135582
|
+
} catch {
|
|
135583
|
+
return false;
|
|
135584
|
+
}
|
|
135585
|
+
};
|
|
135586
|
+
const splitRuns = (t) => splitByCoverage(t, (cp) => covers(cp) ? interFont : dejaFont).map((r) => ({
|
|
135587
|
+
font: r.key,
|
|
135588
|
+
text: r.text
|
|
135589
|
+
}));
|
|
135590
|
+
const measure = (t, s) => splitRuns(t).reduce((sum2, r) => sum2 + r.font.widthOfTextAtSize(r.text, s), 0);
|
|
135591
|
+
const draw = (page, t, x2, y, s, color) => {
|
|
135592
|
+
let cx2 = x2;
|
|
135593
|
+
for (const r of splitRuns(t)) {
|
|
135594
|
+
page.drawText(r.text, { x: cx2, y, size: s, font: r.font, color });
|
|
135595
|
+
cx2 += r.font.widthOfTextAtSize(r.text, s);
|
|
135596
|
+
}
|
|
135597
|
+
};
|
|
135598
|
+
let dejaSet;
|
|
135599
|
+
const hasGlyph = (cp) => {
|
|
135600
|
+
if (covers(cp)) return true;
|
|
135601
|
+
dejaSet ??= new Set(dejaFont.getCharacterSet());
|
|
135602
|
+
return dejaSet.has(cp);
|
|
135603
|
+
};
|
|
135604
|
+
return { measure, draw, hasGlyph };
|
|
135605
|
+
}
|
|
135575
135606
|
async function stampPdf({
|
|
135576
135607
|
pdfBytes,
|
|
135577
135608
|
marksByPage,
|
|
@@ -135605,29 +135636,12 @@ async function stampPdf({
|
|
|
135605
135636
|
return r.arrayBuffer();
|
|
135606
135637
|
})
|
|
135607
135638
|
]);
|
|
135608
|
-
const
|
|
135609
|
-
const dejaFont = await pdfDoc.embedFont(dejaBytes, { subset: true });
|
|
135639
|
+
const faces = await embedStampFaces(pdfDoc, fontkit, { inter: interBytes, deja: dejaBytes });
|
|
135610
135640
|
const pdfPages = pdfDoc.getPages();
|
|
135611
135641
|
const INK = rgb3(0.1, 0.1, 0.1);
|
|
135612
|
-
const
|
|
135613
|
-
const covers = (cp) => {
|
|
135614
|
-
try {
|
|
135615
|
-
return interKit.hasGlyphForCodePoint(cp);
|
|
135616
|
-
} catch {
|
|
135617
|
-
return false;
|
|
135618
|
-
}
|
|
135619
|
-
};
|
|
135620
|
-
const splitRuns = (t) => splitByCoverage(t, (cp) => covers(cp) ? interFont : dejaFont).map((r) => ({
|
|
135621
|
-
font: r.key,
|
|
135622
|
-
text: r.text
|
|
135623
|
-
}));
|
|
135624
|
-
const measure = (t, s) => splitRuns(t).reduce((sum2, r) => sum2 + r.font.widthOfTextAtSize(r.text, s), 0);
|
|
135642
|
+
const { measure } = faces;
|
|
135625
135643
|
const drawMixed = (page, t, x2, y, s) => {
|
|
135626
|
-
|
|
135627
|
-
for (const r of splitRuns(t)) {
|
|
135628
|
-
page.drawText(r.text, { x: cx2, y, size: s, font: r.font, color: INK });
|
|
135629
|
-
cx2 += r.font.widthOfTextAtSize(r.text, s);
|
|
135630
|
-
}
|
|
135644
|
+
faces.draw(page, t, x2, y, s, INK);
|
|
135631
135645
|
};
|
|
135632
135646
|
const drawMarkPdf = (page, shape, r, sizePt, overflow) => {
|
|
135633
135647
|
const fit = Math.min(r.w, r.h);
|
|
@@ -136492,6 +136506,14 @@ function mapInternal(schema, input2, loadImage2) {
|
|
|
136492
136506
|
}
|
|
136493
136507
|
return out;
|
|
136494
136508
|
}
|
|
136509
|
+
function mapAgentValues(schema, input2, opts) {
|
|
136510
|
+
const { values: values2, rowCounts, filled, skipped, warnings } = mapInternal(
|
|
136511
|
+
schema,
|
|
136512
|
+
input2,
|
|
136513
|
+
opts.loadImage
|
|
136514
|
+
);
|
|
136515
|
+
return { values: values2, rowCounts, filled, skipped, warnings };
|
|
136516
|
+
}
|
|
136495
136517
|
async function verifyImages(m) {
|
|
136496
136518
|
if (m.images.length === 0) return;
|
|
136497
136519
|
const { PDFDocument: PDFDocument2 } = await Promise.resolve().then(() => __toESM(require_cjs()));
|
|
@@ -136602,9 +136624,34 @@ function checkTagIds(entries) {
|
|
|
136602
136624
|
return ids;
|
|
136603
136625
|
}
|
|
136604
136626
|
var hits = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
136605
|
-
|
|
136627
|
+
var causeOf = (e4) => e4 instanceof Error && e4.message ? e4.message : String(e4);
|
|
136628
|
+
var oneLine = (s) => s.replace(/[\s\p{Cc}]+/gu, " ").trim();
|
|
136629
|
+
var asciiOnly = (s) => s.replace(/[^\x20-\x7e]/g, "?");
|
|
136630
|
+
var CheckCopyError = class extends Error {
|
|
136631
|
+
at;
|
|
136632
|
+
constructor(at, cause) {
|
|
136633
|
+
super(causeOf(cause));
|
|
136634
|
+
this.name = "CheckCopyError";
|
|
136635
|
+
this.at = { ...at };
|
|
136636
|
+
}
|
|
136637
|
+
};
|
|
136638
|
+
async function buildCheckPdf(stamped, entries, status, fonts, banner = true, tags = true) {
|
|
136639
|
+
const at = { stage: "load" };
|
|
136640
|
+
try {
|
|
136641
|
+
return await drawCheckPdf(stamped, entries, status, fonts, banner, tags, at);
|
|
136642
|
+
} catch (e4) {
|
|
136643
|
+
throw new CheckCopyError(at, e4);
|
|
136644
|
+
}
|
|
136645
|
+
}
|
|
136646
|
+
async function drawCheckPdf(stamped, entries, status, fonts, banner, tags, at) {
|
|
136647
|
+
const step = (stage, id) => {
|
|
136648
|
+
at.stage = stage;
|
|
136649
|
+
at.id = id;
|
|
136650
|
+
};
|
|
136651
|
+
const notes = [];
|
|
136606
136652
|
const { PDFDocument: PDFDocument2, StandardFonts: StandardFonts2, rgb: rgb3 } = await Promise.resolve().then(() => __toESM(require_cjs()));
|
|
136607
136653
|
const doc = await PDFDocument2.load(stamped, { updateMetadata: false });
|
|
136654
|
+
step("fonts");
|
|
136608
136655
|
const helv = await doc.embedFont(StandardFonts2.Helvetica);
|
|
136609
136656
|
const pages = doc.getPages();
|
|
136610
136657
|
const COLORS = {
|
|
@@ -136615,6 +136662,7 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
|
|
|
136615
136662
|
};
|
|
136616
136663
|
const boxes = [];
|
|
136617
136664
|
for (const e4 of entries) {
|
|
136665
|
+
step("outline", e4.id);
|
|
136618
136666
|
const st = status(e4.id);
|
|
136619
136667
|
if (e4.kind === "field") {
|
|
136620
136668
|
boxes.push({ id: e4.id, page: e4.field.page, box: e4.field, st, thin: false, tagged: true });
|
|
@@ -136631,6 +136679,7 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
|
|
|
136631
136679
|
boxes.push({ id: c2.id, page: c2.field.page, box: c2.field, st, thin: true, tagged: false });
|
|
136632
136680
|
}
|
|
136633
136681
|
}
|
|
136682
|
+
step("tag");
|
|
136634
136683
|
const short = shortIds(checkTagIds(entries));
|
|
136635
136684
|
const SIZE = 5;
|
|
136636
136685
|
const rectOf = (b, pw, ph) => {
|
|
@@ -136640,47 +136689,92 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
|
|
|
136640
136689
|
};
|
|
136641
136690
|
const placedTags = /* @__PURE__ */ new Map();
|
|
136642
136691
|
const HEAD = 4;
|
|
136643
|
-
const heads = [];
|
|
136644
136692
|
const headColor = rgb3(0.15, 0.35, 0.75);
|
|
136693
|
+
const wanted = [];
|
|
136645
136694
|
for (const e4 of entries) {
|
|
136646
|
-
if (e4.kind !== "table") continue;
|
|
136695
|
+
if (!tags || e4.kind !== "table") continue;
|
|
136696
|
+
step("table headers", e4.id);
|
|
136647
136697
|
const g = e4.group;
|
|
136648
136698
|
const texts = (v) => Array.isArray(v) ? v : [];
|
|
136699
|
+
const lineOf = (raw) => typeof raw === "string" ? oneLine(raw) : "";
|
|
136649
136700
|
const firstOf = (key, k4) => e4.cells.filter((c2) => c2[key] === k4).sort((a, b) => key === "row" ? a.col - b.col : a.row - b.row).at(0);
|
|
136650
|
-
const
|
|
136651
|
-
|
|
136701
|
+
const want = (axis, index2, raw, cell, below) => {
|
|
136702
|
+
const text = lineOf(raw);
|
|
136703
|
+
if (!text || !cell) return;
|
|
136652
136704
|
const page = pages[cell.field.page - 1];
|
|
136653
|
-
if (
|
|
136654
|
-
const { width: pw, height: ph } = page.getSize();
|
|
136655
|
-
const cx2 = cell.field.xpct / 100 * pw;
|
|
136656
|
-
const top = ph - cell.field.ypct / 100 * ph;
|
|
136657
|
-
const w = cell.field.wpct / 100 * pw;
|
|
136658
|
-
const h = cell.field.hpct / 100 * ph;
|
|
136659
|
-
const inside = w >= 40 && h >= HEAD + 1;
|
|
136660
|
-
const room = inside ? w - 1 : prefix === "R" ? Math.min(120, cx2 - 1) : Math.max(2 * w, 30);
|
|
136661
|
-
let text = `${prefix}: ${raw.trim()}`;
|
|
136662
|
-
while (text.length > 4 && helv.widthOfTextAtSize(text, HEAD) > room) {
|
|
136663
|
-
text = `${text.slice(0, -2)}\u2026`;
|
|
136664
|
-
}
|
|
136665
|
-
const tw = helv.widthOfTextAtSize(text, HEAD);
|
|
136666
|
-
const x2 = inside ? cx2 + 0.6 : prefix === "R" ? cx2 - tw - 0.8 : cx2;
|
|
136667
|
-
const y = inside ? top - HEAD - 0.4 - below : prefix === "R" ? top - h / 2 - HEAD / 2 : top + 0.6;
|
|
136668
|
-
heads.push({ page, text, opts: { x: x2, y, size: HEAD, font: helv, color: headColor } });
|
|
136669
|
-
const taken = placedTags.get(cell.field.page) ?? [];
|
|
136670
|
-
taken.push({ x: x2, y, w: tw, h: HEAD + 0.6 });
|
|
136671
|
-
placedTags.set(cell.field.page, taken);
|
|
136705
|
+
if (page) wanted.push({ gid: e4.id, axis, index: index2, text, cell, page, below });
|
|
136672
136706
|
};
|
|
136673
136707
|
texts(g.header_rows).forEach((t, r) => {
|
|
136674
|
-
|
|
136708
|
+
want("row", r, t, firstOf("row", r), 0);
|
|
136675
136709
|
});
|
|
136676
136710
|
texts(g.header_cols).forEach((t, c2) => {
|
|
136677
136711
|
const cell = firstOf("col", c2);
|
|
136678
136712
|
const shared = cell !== void 0 && cell.col === firstOf("row", cell.row)?.col;
|
|
136679
|
-
const rowHead = cell ? texts(g.header_rows)[cell.row] :
|
|
136680
|
-
|
|
136713
|
+
const rowHead = cell ? lineOf(texts(g.header_rows)[cell.row]) : "";
|
|
136714
|
+
want("column", c2, t, cell, shared && rowHead ? HEAD + 0.6 : 0);
|
|
136681
136715
|
});
|
|
136682
136716
|
}
|
|
136717
|
+
const heads = [];
|
|
136718
|
+
const dropped = (h, e4) => `${h.gid}: ${h.axis} header ${String(h.index)} could not be drawn in the check copy: ${causeOf(e4)}`;
|
|
136719
|
+
if (wanted.length > 0) {
|
|
136720
|
+
step("fonts");
|
|
136721
|
+
const { default: fontkit } = await Promise.resolve().then(() => __toESM(require_fontkit_umd()));
|
|
136722
|
+
doc.registerFontkit(fontkit);
|
|
136723
|
+
const faces = await embedStampFaces(doc, fontkit, fonts);
|
|
136724
|
+
const unreadable = /* @__PURE__ */ new Set();
|
|
136725
|
+
for (const h of wanted) {
|
|
136726
|
+
step("table headers", h.gid);
|
|
136727
|
+
try {
|
|
136728
|
+
const { cell, page } = h;
|
|
136729
|
+
const prefix = h.axis === "row" ? "R" : "C";
|
|
136730
|
+
const { width: pw, height: ph } = page.getSize();
|
|
136731
|
+
const cx2 = cell.field.xpct / 100 * pw;
|
|
136732
|
+
const top = ph - cell.field.ypct / 100 * ph;
|
|
136733
|
+
const w = cell.field.wpct / 100 * pw;
|
|
136734
|
+
const ht = cell.field.hpct / 100 * ph;
|
|
136735
|
+
const inside = w >= 40 && ht >= HEAD + 1;
|
|
136736
|
+
const room = inside ? w - 1 : prefix === "R" ? Math.min(120, cx2 - 1) : Math.max(2 * w, 30);
|
|
136737
|
+
const chars3 = Array.from(
|
|
136738
|
+
`${prefix}: ${h.text}`,
|
|
136739
|
+
(ch) => faces.hasGlyph(ch.codePointAt(0) ?? 0) ? ch : "?"
|
|
136740
|
+
);
|
|
136741
|
+
if (chars3.join("") !== `${prefix}: ${h.text}` && !unreadable.has(h.gid)) {
|
|
136742
|
+
unreadable.add(h.gid);
|
|
136743
|
+
notes.push(
|
|
136744
|
+
`${h.gid}: the check copy cannot draw some header characters (shown as "?"); the legend's header_rows / header_cols hold the full text`
|
|
136745
|
+
);
|
|
136746
|
+
}
|
|
136747
|
+
const cut = (n) => `${chars3.slice(0, n).join("")}\u2026`;
|
|
136748
|
+
let text = chars3.join("");
|
|
136749
|
+
if (chars3.length > 4 && faces.measure(text, HEAD) > room) {
|
|
136750
|
+
let lo = 3;
|
|
136751
|
+
let hi = chars3.length - 2;
|
|
136752
|
+
while (lo < hi) {
|
|
136753
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
136754
|
+
if (faces.measure(cut(mid), HEAD) <= room) lo = mid;
|
|
136755
|
+
else hi = mid - 1;
|
|
136756
|
+
}
|
|
136757
|
+
text = cut(lo);
|
|
136758
|
+
}
|
|
136759
|
+
const tw = faces.measure(text, HEAD);
|
|
136760
|
+
const x2 = inside ? cx2 + 0.6 : prefix === "R" ? cx2 - tw - 0.8 : cx2;
|
|
136761
|
+
const y = inside ? top - HEAD - 0.4 - h.below : prefix === "R" ? top - ht / 2 - HEAD / 2 : top + 0.6;
|
|
136762
|
+
heads.push({
|
|
136763
|
+
head: h,
|
|
136764
|
+
draw: () => {
|
|
136765
|
+
faces.draw(page, text, x2, y, HEAD, headColor);
|
|
136766
|
+
}
|
|
136767
|
+
});
|
|
136768
|
+
const taken = placedTags.get(cell.field.page) ?? [];
|
|
136769
|
+
taken.push({ x: x2, y, w: tw, h: HEAD + 0.6 });
|
|
136770
|
+
placedTags.set(cell.field.page, taken);
|
|
136771
|
+
} catch (e4) {
|
|
136772
|
+
notes.push(dropped(h, e4));
|
|
136773
|
+
}
|
|
136774
|
+
}
|
|
136775
|
+
}
|
|
136683
136776
|
for (const b of boxes) {
|
|
136777
|
+
step("outline", b.id);
|
|
136684
136778
|
const page = pages[b.page - 1];
|
|
136685
136779
|
if (!page) continue;
|
|
136686
136780
|
const { width: pw, height: ph } = page.getSize();
|
|
@@ -136697,8 +136791,9 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
|
|
|
136697
136791
|
opacity: 0,
|
|
136698
136792
|
borderOpacity: 0.9
|
|
136699
136793
|
});
|
|
136700
|
-
if (!b.tagged) continue;
|
|
136701
|
-
|
|
136794
|
+
if (!tags || !b.tagged) continue;
|
|
136795
|
+
step("tag", b.id);
|
|
136796
|
+
const tag = asciiOnly(short.get(b.id) ?? b.id);
|
|
136702
136797
|
const tw = helv.widthOfTextAtSize(tag, SIZE) + 1.6;
|
|
136703
136798
|
const th = SIZE + 1.2;
|
|
136704
136799
|
const right = x2 + w - tw;
|
|
@@ -136730,14 +136825,23 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
|
|
|
136730
136825
|
});
|
|
136731
136826
|
page.drawText(tag, { x: spot.x + 0.8, y: spot.y + 0.8, size: SIZE, font: helv, color });
|
|
136732
136827
|
}
|
|
136733
|
-
for (const h of heads)
|
|
136828
|
+
for (const h of heads) {
|
|
136829
|
+
step("table headers", h.head.gid);
|
|
136830
|
+
try {
|
|
136831
|
+
h.draw();
|
|
136832
|
+
} catch (e4) {
|
|
136833
|
+
notes.push(dropped(h.head, e4));
|
|
136834
|
+
}
|
|
136835
|
+
}
|
|
136836
|
+
step("banner");
|
|
136734
136837
|
const first = pages[0];
|
|
136735
136838
|
if (first && banner) {
|
|
136736
136839
|
const legend = "fenfill check copy: green = filled, orange = filled with a warning, red = skipped, grey = left empty. Tags are field-id prefixes.";
|
|
136737
136840
|
const { height: ph } = first.getSize();
|
|
136738
136841
|
first.drawText(legend, { x: 6, y: ph - 7, size: SIZE, font: helv, color: rgb3(0.3, 0.3, 0.35) });
|
|
136739
136842
|
}
|
|
136740
|
-
|
|
136843
|
+
step("save");
|
|
136844
|
+
return { pdf: await doc.save(), notes };
|
|
136741
136845
|
}
|
|
136742
136846
|
async function fillAgentPdf(a) {
|
|
136743
136847
|
if (!isRecord(a.render)) throw new TypeError("render must be the template's schema object");
|
|
@@ -136772,12 +136876,21 @@ async function fillAgentPdf(a) {
|
|
|
136772
136876
|
const warned = new Set(
|
|
136773
136877
|
placed.flatMap((w) => w.slice(0, w.indexOf(":")).split(" and ").map(topId))
|
|
136774
136878
|
);
|
|
136775
|
-
|
|
136776
|
-
|
|
136777
|
-
|
|
136778
|
-
|
|
136779
|
-
|
|
136780
|
-
|
|
136879
|
+
try {
|
|
136880
|
+
const built = await buildCheckPdf(
|
|
136881
|
+
pdf,
|
|
136882
|
+
m.entries,
|
|
136883
|
+
(id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
|
|
136884
|
+
a.fonts,
|
|
136885
|
+
a.checkBanner !== false,
|
|
136886
|
+
a.checkTags !== false
|
|
136887
|
+
);
|
|
136888
|
+
res.checkPdf = built.pdf;
|
|
136889
|
+
warnings.push(...built.notes);
|
|
136890
|
+
} catch (e4) {
|
|
136891
|
+
const at = e4 instanceof CheckCopyError ? e4.at : { stage: "build" };
|
|
136892
|
+
res.checkError = { stage: at.stage, ...at.id ? { id: at.id } : {}, message: causeOf(e4) };
|
|
136893
|
+
}
|
|
136781
136894
|
}
|
|
136782
136895
|
return res;
|
|
136783
136896
|
}
|
|
@@ -137374,19 +137487,24 @@ function listedPages(agent, requested) {
|
|
|
137374
137487
|
return have.filter((p4) => want.has(p4));
|
|
137375
137488
|
}
|
|
137376
137489
|
function nestBySection(entries) {
|
|
137377
|
-
const blocks =
|
|
137490
|
+
const blocks = [];
|
|
137491
|
+
let run = null;
|
|
137378
137492
|
for (const e4 of entries) {
|
|
137379
|
-
const sid = e4.section?.id
|
|
137380
|
-
const key = `${String(e4.page)}\0${sid}`;
|
|
137381
|
-
|
|
137382
|
-
if (!b) {
|
|
137493
|
+
const sid = e4.section?.id;
|
|
137494
|
+
const key = `${String(e4.page)}\0${sid ?? ""}`;
|
|
137495
|
+
if (key !== run) {
|
|
137383
137496
|
const title = e4.section?.title;
|
|
137384
|
-
|
|
137385
|
-
|
|
137497
|
+
blocks.push({
|
|
137498
|
+
page: e4.page,
|
|
137499
|
+
...sid ? { section_id: sid } : {},
|
|
137500
|
+
...title ? { title } : {},
|
|
137501
|
+
fields: []
|
|
137502
|
+
});
|
|
137503
|
+
run = key;
|
|
137386
137504
|
}
|
|
137387
|
-
|
|
137505
|
+
blocks[blocks.length - 1].fields.push(compactEntry(e4));
|
|
137388
137506
|
}
|
|
137389
|
-
return
|
|
137507
|
+
return blocks;
|
|
137390
137508
|
}
|
|
137391
137509
|
function compactView(agent, requested, next, maxChars = COMPACT_MAX_CHARS) {
|
|
137392
137510
|
const entries = selectEntries(agent, requested);
|
|
@@ -137680,7 +137798,7 @@ function loadFonts(dir = defaultFontsDir()) {
|
|
|
137680
137798
|
}
|
|
137681
137799
|
|
|
137682
137800
|
// src/version.ts
|
|
137683
|
-
var VERSION = true ? "0.2.
|
|
137801
|
+
var VERSION = true ? "0.2.2" : "0.0.0-dev";
|
|
137684
137802
|
var USER_AGENT = `fenfill-mcp/${VERSION}`;
|
|
137685
137803
|
|
|
137686
137804
|
// src/fill.ts
|
|
@@ -137864,6 +137982,11 @@ function fillForbiddenError() {
|
|
|
137864
137982
|
hint: "fenfill can't fill this file; use a copy whose permissions allow form filling."
|
|
137865
137983
|
});
|
|
137866
137984
|
}
|
|
137985
|
+
function checkCopyFailed(err2) {
|
|
137986
|
+
const what = "The check copy could not be built";
|
|
137987
|
+
if (!err2) return `${what}.`;
|
|
137988
|
+
return `${what} (${[err2.stage, err2.id].filter(Boolean).join(", ")}): ${err2.message}`;
|
|
137989
|
+
}
|
|
137867
137990
|
async function checkPathFor(outputAbs, opts) {
|
|
137868
137991
|
return resolveOutputPath(outputAbs.replace(/\.pdf$/i, ".check.pdf"), opts);
|
|
137869
137992
|
}
|
|
@@ -137889,17 +138012,22 @@ async function stampAndWrite(a) {
|
|
|
137889
138012
|
await writeOutputAtomic(a.outputAbs, res.pdf, a.overwrite);
|
|
137890
138013
|
await a.cache.registerOutput(sha256Hex(res.pdf));
|
|
137891
138014
|
let checkPath;
|
|
138015
|
+
const warnings = [...res.warnings];
|
|
137892
138016
|
if (a.checkAbs && res.checkPdf) {
|
|
137893
138017
|
await writeOutputAtomic(a.checkAbs, res.checkPdf, a.overwrite);
|
|
137894
138018
|
await a.cache.registerOutput(sha256Hex(res.checkPdf));
|
|
137895
138019
|
checkPath = a.checkAbs;
|
|
138020
|
+
} else if (a.checkAbs) {
|
|
138021
|
+
warnings.push(
|
|
138022
|
+
`No check copy was written; the filled PDF is complete. ${checkCopyFailed(res.checkError)}`
|
|
138023
|
+
);
|
|
137896
138024
|
}
|
|
137897
138025
|
return {
|
|
137898
138026
|
output_path: a.outputAbs,
|
|
137899
138027
|
...checkPath ? { check_path: checkPath } : {},
|
|
137900
138028
|
filled: res.filled,
|
|
137901
138029
|
skipped: res.skipped,
|
|
137902
|
-
warnings
|
|
138030
|
+
warnings
|
|
137903
138031
|
};
|
|
137904
138032
|
}
|
|
137905
138033
|
|
|
@@ -139193,67 +139321,208 @@ var ECHO_CONTAINS_MIN = 6;
|
|
|
139193
139321
|
var MAX_REMEMBERED = 5e3;
|
|
139194
139322
|
var MAX_LEN = 2e3;
|
|
139195
139323
|
var FREE_TEXT = /* @__PURE__ */ new Set(["text", "multiline", "date", "comb"]);
|
|
139196
|
-
var norm = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
139324
|
+
var norm = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
139325
|
+
var isRec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
|
|
139326
|
+
var NO_IMAGE = () => {
|
|
139327
|
+
throw new Error("not read by the echo guard");
|
|
139328
|
+
};
|
|
139329
|
+
function inPrintedFormat(digits2, fmt3) {
|
|
139330
|
+
const d = Array.from(digits2);
|
|
139331
|
+
const cells = Array.from(fmt3);
|
|
139332
|
+
const isLetter = (c2) => HAS_LETTER.test(c2);
|
|
139333
|
+
if (!d.length || !d.every((c2) => new RegExp("^\\p{Nd}$", "u").test(c2))) return "";
|
|
139334
|
+
if (d.length !== cells.filter(isLetter).length) return "";
|
|
139335
|
+
let i2 = 0;
|
|
139336
|
+
return cells.map((c2) => isLetter(c2) ? d[i2++] : c2).join("");
|
|
139337
|
+
}
|
|
139338
|
+
var LETTER = "[\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{M}]";
|
|
139339
|
+
var HAS_LETTER = new RegExp(LETTER, "u");
|
|
139340
|
+
var DIGIT = "\\p{Nd}";
|
|
139341
|
+
var ENDS = [new RegExp(`${LETTER}$`, "u"), new RegExp(`${DIGIT}$`, "u")];
|
|
139342
|
+
var STARTS = [new RegExp(`^${LETTER}`, "u"), new RegExp(`^${DIGIT}`, "u")];
|
|
139343
|
+
var runsOn = (left, right) => ENDS.some((end, i2) => end.test(left) && STARTS[i2].test(right));
|
|
139344
|
+
function holds(n, v) {
|
|
139345
|
+
if (v.includes(" ") || !HAS_LETTER.test(v)) return true;
|
|
139346
|
+
for (let i2 = n.indexOf(v); i2 !== -1; i2 = n.indexOf(v, i2 + 1)) {
|
|
139347
|
+
if (!runsOn(n.slice(0, i2), v) && !runsOn(v, n.slice(i2 + v.length))) return true;
|
|
139348
|
+
}
|
|
139349
|
+
return false;
|
|
139350
|
+
}
|
|
139351
|
+
var entryKey = (w) => w.page === void 0 ? w.id : `${w.id}\0${String(w.page)}`;
|
|
139352
|
+
var keyOf = (r) => `${r.prop}\0${norm(r.text)}`;
|
|
139353
|
+
var placeOf = (r) => `${r.scope ?? ""}\0${entryKey(r)}\0${keyOf(r)}`;
|
|
139354
|
+
function keep(set2, key) {
|
|
139355
|
+
set2.delete(key);
|
|
139356
|
+
set2.add(key);
|
|
139357
|
+
if (set2.size > MAX_REMEMBERED) {
|
|
139358
|
+
const oldest = set2.values().next().value;
|
|
139359
|
+
if (oldest !== void 0) set2.delete(oldest);
|
|
139360
|
+
}
|
|
139361
|
+
}
|
|
139197
139362
|
var EchoGuard = class {
|
|
139198
139363
|
seen = /* @__PURE__ */ new Set();
|
|
139364
|
+
/** Refused wording the agent has been told about (property + text), and the
|
|
139365
|
+
* entries it was named on (working copy + entry + property + text). */
|
|
139366
|
+
told = /* @__PURE__ */ new Set();
|
|
139367
|
+
toldAt = /* @__PURE__ */ new Set();
|
|
139368
|
+
/** Wording the agent confirmed as the form's own (property + text: an id
|
|
139369
|
+
* does not survive group / ungroup, and the wording is what was vouched). */
|
|
139370
|
+
vouched = /* @__PURE__ */ new Set();
|
|
139199
139371
|
add(v) {
|
|
139200
139372
|
if (typeof v === "number" && Number.isFinite(v)) v = String(v);
|
|
139201
139373
|
if (typeof v !== "string") return;
|
|
139202
139374
|
const n = norm(v);
|
|
139203
139375
|
if (!n || n.length > MAX_LEN) return;
|
|
139204
|
-
this.seen
|
|
139205
|
-
|
|
139206
|
-
|
|
139207
|
-
const
|
|
139208
|
-
if (
|
|
139376
|
+
keep(this.seen, n);
|
|
139377
|
+
if (!/[\r\n]/.test(v)) return;
|
|
139378
|
+
for (const line of v.split(/\r\n|\r|\n/)) {
|
|
139379
|
+
const l = norm(line);
|
|
139380
|
+
if (l.length >= ECHO_REFUSE_MIN && l.length <= MAX_LEN) keep(this.seen, l);
|
|
139381
|
+
}
|
|
139382
|
+
}
|
|
139383
|
+
/** Every text in `v`, whatever it was meant for (no entry tells a free-text
|
|
139384
|
+
* answer from a choice here), from the length a match refuses at. */
|
|
139385
|
+
addAny(v, depth = 0) {
|
|
139386
|
+
if (depth > 4) return;
|
|
139387
|
+
if (Array.isArray(v)) for (const x2 of v) this.addAny(x2, depth + 1);
|
|
139388
|
+
else if (isRec(v)) for (const x2 of Object.values(v)) this.addAny(x2, depth + 1);
|
|
139389
|
+
else {
|
|
139390
|
+
const s = typeof v === "number" && Number.isFinite(v) ? String(v) : v;
|
|
139391
|
+
if (typeof s === "string" && norm(s).length >= ECHO_REFUSE_MIN) this.add(s);
|
|
139392
|
+
}
|
|
139393
|
+
}
|
|
139394
|
+
/** Remember the values of a call that failed before its form was read (so
|
|
139395
|
+
* `remember` never saw them): the agent has typed them all the same, and one
|
|
139396
|
+
* can come back as wording on its next call. */
|
|
139397
|
+
rememberUnread(values2) {
|
|
139398
|
+
this.addAny(values2);
|
|
139399
|
+
}
|
|
139400
|
+
/** The same answers as the page shows them: the text of every mark the core
|
|
139401
|
+
* would stamp for these values (a date in its field's format, text as
|
|
139402
|
+
* normalized), a comb's cells joined into one answer. */
|
|
139403
|
+
addShown(schema, entries, values2) {
|
|
139404
|
+
const shown = /* @__PURE__ */ new Map();
|
|
139405
|
+
try {
|
|
139406
|
+
const mapped = mapAgentValues(schema, values2, { loadImage: NO_IMAGE });
|
|
139407
|
+
const marks = buildMarks({
|
|
139408
|
+
fields: schema.fields,
|
|
139409
|
+
groups: schema.groups,
|
|
139410
|
+
values: mapped.values,
|
|
139411
|
+
rowCounts: mapped.rowCounts
|
|
139412
|
+
});
|
|
139413
|
+
for (const page of marks.values()) {
|
|
139414
|
+
for (const m of page) {
|
|
139415
|
+
if (m.image || m.checkbox) continue;
|
|
139416
|
+
const key = m.key.split("#")[0];
|
|
139417
|
+
shown.set(key, (shown.get(key) ?? "") + m.text);
|
|
139418
|
+
}
|
|
139419
|
+
}
|
|
139420
|
+
} catch {
|
|
139421
|
+
return;
|
|
139422
|
+
}
|
|
139423
|
+
for (const e4 of entries) {
|
|
139424
|
+
if (e4.kind !== "comb") continue;
|
|
139425
|
+
let cells = "";
|
|
139426
|
+
for (const s of e4.slots) {
|
|
139427
|
+
cells += shown.get(s.key) ?? "";
|
|
139428
|
+
shown.delete(s.key);
|
|
139429
|
+
}
|
|
139430
|
+
this.add(cells);
|
|
139431
|
+
if (e4.dateFormat) this.add(inPrintedFormat(cells, e4.dateFormat));
|
|
139209
139432
|
}
|
|
139433
|
+
for (const [, text] of shown) this.add(text);
|
|
139210
139434
|
}
|
|
139211
139435
|
/** Remember the free-text values of one fill/preview call. */
|
|
139212
139436
|
remember(render, values2) {
|
|
139437
|
+
let schema;
|
|
139213
139438
|
let entries;
|
|
139214
139439
|
try {
|
|
139215
|
-
|
|
139440
|
+
schema = normalizeSchema(render);
|
|
139441
|
+
entries = agentEntries(schema);
|
|
139216
139442
|
} catch {
|
|
139443
|
+
this.addAny(values2);
|
|
139217
139444
|
return;
|
|
139218
139445
|
}
|
|
139219
|
-
|
|
139446
|
+
this.addShown(schema, entries, values2);
|
|
139220
139447
|
for (const e4 of entries) {
|
|
139221
139448
|
const v = values2[e4.id];
|
|
139222
139449
|
if (v === void 0) continue;
|
|
139223
139450
|
if (e4.kind === "field" || e4.kind === "comb") {
|
|
139224
139451
|
if (FREE_TEXT.has(e4.type)) this.add(v);
|
|
139225
|
-
} else if (e4.kind === "table" &&
|
|
139452
|
+
} else if (e4.kind === "table" && isRec(v)) {
|
|
139226
139453
|
for (const c2 of e4.cells) if (FREE_TEXT.has(c2.type)) this.add(v[c2.id]);
|
|
139227
139454
|
} else if (e4.kind === "table_rows" && Array.isArray(v)) {
|
|
139455
|
+
const ids = new Set(e4.columns.map((c2) => c2.id));
|
|
139228
139456
|
for (const row of v) {
|
|
139229
|
-
if (!
|
|
139457
|
+
if (!isRec(row)) continue;
|
|
139230
139458
|
for (const c2 of e4.columns) if (FREE_TEXT.has(c2.type)) this.add(row[c2.id]);
|
|
139459
|
+
for (const [k4, cell] of Object.entries(row)) if (!ids.has(k4)) this.add(cell);
|
|
139231
139460
|
}
|
|
139232
139461
|
}
|
|
139233
139462
|
}
|
|
139234
139463
|
}
|
|
139235
|
-
/** Wording that repeats a remembered answer: refusals (it
|
|
139236
|
-
*
|
|
139237
|
-
|
|
139464
|
+
/** Wording that repeats a remembered answer: refusals (it holds one of 6+
|
|
139465
|
+
* chars, or equals one of 4+) and warnings (equals a shorter one). Wording
|
|
139466
|
+
* the agent confirmed is listed apart and refuses nothing. `scope` is the
|
|
139467
|
+
* working copy the wording is in: an entry named in one copy is not thereby
|
|
139468
|
+
* named in another (section ids, and a copied template's ids, repeat). */
|
|
139469
|
+
check(wording, scope = "") {
|
|
139238
139470
|
const refuse = /* @__PURE__ */ new Set();
|
|
139239
139471
|
const warn = /* @__PURE__ */ new Set();
|
|
139240
|
-
|
|
139472
|
+
const refused = [];
|
|
139473
|
+
const confirmed = [];
|
|
139474
|
+
if (this.seen.size === 0) return { refuse: [], warn: [], refused, confirmed };
|
|
139241
139475
|
for (const w of wording) {
|
|
139242
139476
|
const n = norm(w.text);
|
|
139243
139477
|
if (!n) continue;
|
|
139478
|
+
let hit = false;
|
|
139244
139479
|
if (this.seen.has(n)) {
|
|
139245
|
-
(n.length
|
|
139246
|
-
|
|
139247
|
-
|
|
139248
|
-
if (n.length < ECHO_CONTAINS_MIN) continue;
|
|
139249
|
-
for (const v of this.seen) {
|
|
139250
|
-
if (v.length >= ECHO_CONTAINS_MIN && n.includes(v)) {
|
|
139251
|
-
refuse.add(w.id);
|
|
139252
|
-
break;
|
|
139480
|
+
if (n.length < ECHO_REFUSE_MIN) {
|
|
139481
|
+
warn.add(w.id);
|
|
139482
|
+
continue;
|
|
139253
139483
|
}
|
|
139484
|
+
hit = true;
|
|
139485
|
+
} else if (n.length >= ECHO_CONTAINS_MIN) {
|
|
139486
|
+
for (const v of this.seen) {
|
|
139487
|
+
if (v.length >= ECHO_CONTAINS_MIN && n.includes(v) && holds(n, v)) {
|
|
139488
|
+
hit = true;
|
|
139489
|
+
break;
|
|
139490
|
+
}
|
|
139491
|
+
}
|
|
139492
|
+
}
|
|
139493
|
+
if (!hit) continue;
|
|
139494
|
+
const r = {
|
|
139495
|
+
id: w.id,
|
|
139496
|
+
...w.page === void 0 ? {} : { page: w.page },
|
|
139497
|
+
prop: w.prop ?? "",
|
|
139498
|
+
text: w.text,
|
|
139499
|
+
...scope ? { scope } : {}
|
|
139500
|
+
};
|
|
139501
|
+
const key = keyOf(r);
|
|
139502
|
+
if (this.vouched.has(key)) confirmed.push({ ...r, shown: true, listed: true });
|
|
139503
|
+
else {
|
|
139504
|
+
refuse.add(w.id);
|
|
139505
|
+
refused.push({ ...r, shown: this.told.has(key), listed: this.toldAt.has(placeOf(r)) });
|
|
139254
139506
|
}
|
|
139255
139507
|
}
|
|
139256
|
-
return {
|
|
139508
|
+
return {
|
|
139509
|
+
refuse: [...refuse],
|
|
139510
|
+
warn: [...warn].filter((id) => !refuse.has(id)),
|
|
139511
|
+
refused,
|
|
139512
|
+
confirmed
|
|
139513
|
+
};
|
|
139514
|
+
}
|
|
139515
|
+
/** The agent has now been told about these refusals (an op's rejection, a
|
|
139516
|
+
* save's answer_in_template): from here on it can confirm them. */
|
|
139517
|
+
tell(refused) {
|
|
139518
|
+
for (const r of refused) {
|
|
139519
|
+
keep(this.told, keyOf(r));
|
|
139520
|
+
keep(this.toldAt, placeOf(r));
|
|
139521
|
+
}
|
|
139522
|
+
}
|
|
139523
|
+
/** The agent confirmed these as the form's own wording. */
|
|
139524
|
+
confirm(refused) {
|
|
139525
|
+
for (const r of refused) keep(this.vouched, keyOf(r));
|
|
139257
139526
|
}
|
|
139258
139527
|
get size() {
|
|
139259
139528
|
return this.seen.size;
|
|
@@ -139263,13 +139532,121 @@ var EchoGuard = class {
|
|
|
139263
139532
|
// src/edits.ts
|
|
139264
139533
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
139265
139534
|
|
|
139535
|
+
// ../../app/t/[template_id]/readOrder.ts
|
|
139536
|
+
var LINE_OVERLAP_FRAC = 0.5;
|
|
139537
|
+
var LEFT_OF_SLACK_PCT = 0.5;
|
|
139538
|
+
function boxEdges(box) {
|
|
139539
|
+
return {
|
|
139540
|
+
left: box.xpct,
|
|
139541
|
+
top: box.ypct,
|
|
139542
|
+
right: box.xpct + box.wpct,
|
|
139543
|
+
bottom: box.ypct + box.hpct
|
|
139544
|
+
};
|
|
139545
|
+
}
|
|
139546
|
+
function membersEdges(members3) {
|
|
139547
|
+
if (members3.length === 0) return null;
|
|
139548
|
+
let x0 = Infinity;
|
|
139549
|
+
let y0 = Infinity;
|
|
139550
|
+
let x1 = -Infinity;
|
|
139551
|
+
let y1 = -Infinity;
|
|
139552
|
+
for (const m of members3) {
|
|
139553
|
+
x0 = Math.min(x0, m.xpct);
|
|
139554
|
+
y0 = Math.min(y0, m.ypct);
|
|
139555
|
+
x1 = Math.max(x1, m.xpct + m.wpct);
|
|
139556
|
+
y1 = Math.max(y1, m.ypct + m.hpct);
|
|
139557
|
+
}
|
|
139558
|
+
return boxEdges({ xpct: x0, ypct: y0, wpct: x1 - x0, hpct: y1 - y0 });
|
|
139559
|
+
}
|
|
139560
|
+
function sameLine(a, b) {
|
|
139561
|
+
const overlap2 = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
|
139562
|
+
const small = Math.min(a.bottom - a.top, b.bottom - b.top);
|
|
139563
|
+
return small > 0 && overlap2 >= LINE_OVERLAP_FRAC * small;
|
|
139564
|
+
}
|
|
139565
|
+
function leftOf(a, b) {
|
|
139566
|
+
return a.left < b.left && a.right <= b.left + LEFT_OF_SLACK_PCT;
|
|
139567
|
+
}
|
|
139568
|
+
function readingLines(blocks) {
|
|
139569
|
+
const lines = [];
|
|
139570
|
+
let latest;
|
|
139571
|
+
for (const b of [...blocks].sort((p4, q) => p4.top - q.top || p4.left - q.left)) {
|
|
139572
|
+
if (latest?.every((o) => sameLine(b, o))) {
|
|
139573
|
+
latest.push(b);
|
|
139574
|
+
} else {
|
|
139575
|
+
latest = [b];
|
|
139576
|
+
lines.push(latest);
|
|
139577
|
+
}
|
|
139578
|
+
}
|
|
139579
|
+
return lines.map((ln2) => {
|
|
139580
|
+
const row = [];
|
|
139581
|
+
for (const b of ln2) {
|
|
139582
|
+
const at = row.findIndex((o) => leftOf(b, o));
|
|
139583
|
+
row.splice(at < 0 ? row.length : at, 0, b);
|
|
139584
|
+
}
|
|
139585
|
+
return row;
|
|
139586
|
+
});
|
|
139587
|
+
}
|
|
139588
|
+
function orderBlocks(blocks) {
|
|
139589
|
+
if (!blocks.some((b) => b.order != null)) return readingLines(blocks).flat();
|
|
139590
|
+
return [...blocks].sort((a, b) => {
|
|
139591
|
+
const oa = a.order ?? Number.POSITIVE_INFINITY;
|
|
139592
|
+
const ob = b.order ?? Number.POSITIVE_INFINITY;
|
|
139593
|
+
if (oa !== ob) return oa - ob;
|
|
139594
|
+
return a.top - b.top || a.left - b.left;
|
|
139595
|
+
});
|
|
139596
|
+
}
|
|
139597
|
+
|
|
139598
|
+
// ../../app/t/[template_id]/pageLayout.ts
|
|
139599
|
+
var orderOf = (b) => b.kind === "field" ? b.field.order : b.group.order;
|
|
139600
|
+
function buildPageItems(page, pageFields, pageGroups, resolve3) {
|
|
139601
|
+
const placed = [];
|
|
139602
|
+
const place = (block, edges) => {
|
|
139603
|
+
placed.push({ ...edges, block, order: orderOf(block) });
|
|
139604
|
+
};
|
|
139605
|
+
for (const f3 of pageFields) {
|
|
139606
|
+
place({ kind: "field", top: f3.ypct, left: f3.xpct, key: `f${f3.id}`, field: f3 }, boxEdges(f3));
|
|
139607
|
+
}
|
|
139608
|
+
for (const g of pageGroups) {
|
|
139609
|
+
const members3 = resolve3(g);
|
|
139610
|
+
const edges = membersEdges(members3);
|
|
139611
|
+
if (!edges) continue;
|
|
139612
|
+
const { top, left } = edges;
|
|
139613
|
+
place({ kind: "group", top, left, key: `g${g.id}`, group: g, members: members3 }, edges);
|
|
139614
|
+
}
|
|
139615
|
+
const sectionOf2 = (b) => b.kind === "field" ? b.field.section_id : b.members[0]?.section_id ?? null;
|
|
139616
|
+
const sorted = orderBlocks(placed).map((p4) => p4.block);
|
|
139617
|
+
const secById = new Map(page.sections.map((s) => [s.id, s]));
|
|
139618
|
+
const units = [];
|
|
139619
|
+
const sectionUnit = /* @__PURE__ */ new Map();
|
|
139620
|
+
for (const b of sorted) {
|
|
139621
|
+
const sid = sectionOf2(b);
|
|
139622
|
+
const sec = sid != null ? secById.get(sid) : void 0;
|
|
139623
|
+
if (!sec) {
|
|
139624
|
+
units.push({ section: null, blocks: [b] });
|
|
139625
|
+
continue;
|
|
139626
|
+
}
|
|
139627
|
+
let u = sectionUnit.get(sec.id);
|
|
139628
|
+
if (!u) {
|
|
139629
|
+
u = { section: sec, blocks: [] };
|
|
139630
|
+
sectionUnit.set(sec.id, u);
|
|
139631
|
+
units.push(u);
|
|
139632
|
+
}
|
|
139633
|
+
u.blocks.push(b);
|
|
139634
|
+
}
|
|
139635
|
+
const items2 = [];
|
|
139636
|
+
for (const u of units) {
|
|
139637
|
+
if (u.section) items2.push({ kind: "header", key: `h${u.section.id}`, section: u.section });
|
|
139638
|
+
for (const b of u.blocks) items2.push(b);
|
|
139639
|
+
}
|
|
139640
|
+
return { page, items: items2 };
|
|
139641
|
+
}
|
|
139642
|
+
|
|
139266
139643
|
// src/doc.ts
|
|
139267
139644
|
var OpError = class extends Error {
|
|
139268
139645
|
};
|
|
139269
139646
|
var bad = (msg) => {
|
|
139270
139647
|
throw new OpError(msg);
|
|
139271
139648
|
};
|
|
139272
|
-
var
|
|
139649
|
+
var isRec2 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
|
|
139273
139650
|
var isNum = (x2) => typeof x2 === "number" && Number.isFinite(x2);
|
|
139274
139651
|
var round = (n, d = 4) => Math.round(n * 10 ** d) / 10 ** d;
|
|
139275
139652
|
function index(s) {
|
|
@@ -139383,7 +139760,7 @@ function checkBoxOnPage(b) {
|
|
|
139383
139760
|
if (!onPage(b)) bad(ON_PAGE);
|
|
139384
139761
|
}
|
|
139385
139762
|
function boxArg(v, name = "box") {
|
|
139386
|
-
if (!
|
|
139763
|
+
if (!isRec2(v)) return bad(`${name} {x, y, w, h} is required (fractions of the page, top-left)`);
|
|
139387
139764
|
const b = {
|
|
139388
139765
|
x: checkFraction(v.x, `${name}.x`),
|
|
139389
139766
|
y: checkFraction(v.y, `${name}.y`),
|
|
@@ -139461,7 +139838,7 @@ function patchForDataType(t, dateFormat) {
|
|
|
139461
139838
|
}
|
|
139462
139839
|
var DEFAULT_TEXT = { type: "text", format: { variant: "any" } };
|
|
139463
139840
|
function variantOf(f3) {
|
|
139464
|
-
return
|
|
139841
|
+
return isRec2(f3.format) && typeof f3.format.variant === "string" ? f3.format.variant : "any";
|
|
139465
139842
|
}
|
|
139466
139843
|
function isDefaultText2(f3) {
|
|
139467
139844
|
return f3.type === "text" && variantOf(f3) === "any";
|
|
@@ -139573,7 +139950,7 @@ function lineType(d, v, i2) {
|
|
|
139573
139950
|
const w = cells.find((c2) => !isDefaultText2(c2)) ?? cells[0];
|
|
139574
139951
|
return {
|
|
139575
139952
|
type: String(w.type),
|
|
139576
|
-
...
|
|
139953
|
+
...isRec2(w.format) ? { format: structuredClone(w.format) } : {}
|
|
139577
139954
|
};
|
|
139578
139955
|
}
|
|
139579
139956
|
var axisWord = (a) => a === "col" ? "column" : "row";
|
|
@@ -139848,7 +140225,7 @@ function tableInsert(d, op, w) {
|
|
|
139848
140225
|
}
|
|
139849
140226
|
let at;
|
|
139850
140227
|
if (op.box !== void 0) {
|
|
139851
|
-
if (!
|
|
140228
|
+
if (!isRec2(op.box)) return bad("box must be an object");
|
|
139852
140229
|
const b = op.box;
|
|
139853
140230
|
const [p4, s] = axis === "row" ? ["y", "h"] : ["x", "w"];
|
|
139854
140231
|
const pos = b[p4];
|
|
@@ -139919,8 +140296,8 @@ function tableDelete(d, op, w) {
|
|
|
139919
140296
|
}
|
|
139920
140297
|
if (v.cols === 0) v.grid = [];
|
|
139921
140298
|
v.rows = v.grid.length;
|
|
139922
|
-
const
|
|
139923
|
-
if (
|
|
140299
|
+
const keep2 = op.keep_cells === true;
|
|
140300
|
+
if (keep2) {
|
|
139924
140301
|
for (const id of ids) {
|
|
139925
140302
|
const f3 = d.field.get(id);
|
|
139926
140303
|
if (f3) f3.group = null;
|
|
@@ -139929,9 +140306,9 @@ function tableDelete(d, op, w) {
|
|
|
139929
140306
|
} else removeFields(d, ids);
|
|
139930
140307
|
const alive = commit(d, v, hc, hr);
|
|
139931
140308
|
if (!alive) w.push(`that was the last line of table ${v.g.id}, so the table was removed`);
|
|
139932
|
-
const deleted = [...
|
|
140309
|
+
const deleted = [...keep2 ? [] : ids, ...alive ? [] : [v.g.id]];
|
|
139933
140310
|
return {
|
|
139934
|
-
touched: [...alive ? [v.g.id] : [], ...
|
|
140311
|
+
touched: [...alive ? [v.g.id] : [], ...keep2 ? ids : []],
|
|
139935
140312
|
deleted,
|
|
139936
140313
|
tables: alive ? [v.g.id] : []
|
|
139937
140314
|
};
|
|
@@ -139982,7 +140359,7 @@ function tableAdopt(d, op, w) {
|
|
|
139982
140359
|
const { r, c: c2 } = slotArgs(v, op);
|
|
139983
140360
|
const tf = slotType(d, v, r, c2, {
|
|
139984
140361
|
type: String(f3.type),
|
|
139985
|
-
...
|
|
140362
|
+
...isRec2(f3.format) ? { format: f3.format } : {}
|
|
139986
140363
|
});
|
|
139987
140364
|
if (!sameType(f3, tf)) {
|
|
139988
140365
|
w.push(
|
|
@@ -140221,7 +140598,7 @@ var FORMAT_KEYS = {
|
|
|
140221
140598
|
date: /* @__PURE__ */ new Set(["date_format", "font_size", "text_anchor"]),
|
|
140222
140599
|
signature: /* @__PURE__ */ new Set(["scale", "text_anchor"])
|
|
140223
140600
|
};
|
|
140224
|
-
var
|
|
140601
|
+
var isRec3 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
|
|
140225
140602
|
var isNum2 = (x2) => typeof x2 === "number" && Number.isFinite(x2);
|
|
140226
140603
|
var isInt = (x2) => isNum2(x2) && Number.isInteger(x2);
|
|
140227
140604
|
function checkStr(out, v, max2, at, optional2) {
|
|
@@ -140254,7 +140631,7 @@ function checkAnchor(out, v, at) {
|
|
|
140254
140631
|
function formatIssues(type, fmt3, id, path) {
|
|
140255
140632
|
const out = [];
|
|
140256
140633
|
if (fmt3 === void 0 || fmt3 === null) return out;
|
|
140257
|
-
if (!
|
|
140634
|
+
if (!isRec3(fmt3)) return [{ id, path, reason: "format must be an object" }];
|
|
140258
140635
|
const allowed = typeof type === "string" ? FORMAT_KEYS[type] : void 0;
|
|
140259
140636
|
if (!allowed) return out;
|
|
140260
140637
|
for (const k4 of Object.keys(fmt3)) {
|
|
@@ -140291,7 +140668,7 @@ function formatIssues(type, fmt3, id, path) {
|
|
|
140291
140668
|
return out;
|
|
140292
140669
|
}
|
|
140293
140670
|
function fieldIssues(f3, path) {
|
|
140294
|
-
if (!
|
|
140671
|
+
if (!isRec3(f3)) return [{ path, reason: "must be an object" }];
|
|
140295
140672
|
const id = typeof f3.id === "string" ? f3.id : void 0;
|
|
140296
140673
|
const out = [];
|
|
140297
140674
|
const at = (k4) => ({ id, path: `${path}.${k4}` });
|
|
@@ -140314,7 +140691,7 @@ function fieldIssues(f3, path) {
|
|
|
140314
140691
|
else {
|
|
140315
140692
|
f3.cells.forEach((c2, i2) => {
|
|
140316
140693
|
const cp = { id, path: `${path}.cells.${String(i2)}` };
|
|
140317
|
-
if (!
|
|
140694
|
+
if (!isRec3(c2)) {
|
|
140318
140695
|
out.push({ ...cp, reason: "must be an object" });
|
|
140319
140696
|
return;
|
|
140320
140697
|
}
|
|
@@ -140340,7 +140717,7 @@ function fieldIssues(f3, path) {
|
|
|
140340
140717
|
return out;
|
|
140341
140718
|
}
|
|
140342
140719
|
function groupIssues(g, path) {
|
|
140343
|
-
if (!
|
|
140720
|
+
if (!isRec3(g)) return [{ path, reason: "must be an object" }];
|
|
140344
140721
|
const id = typeof g.id === "string" ? g.id : void 0;
|
|
140345
140722
|
const out = [];
|
|
140346
140723
|
const at = (k4) => ({ id, path: `${path}.${k4}` });
|
|
@@ -140398,7 +140775,7 @@ function groupIssues(g, path) {
|
|
|
140398
140775
|
return out;
|
|
140399
140776
|
}
|
|
140400
140777
|
function sectionIssues(s, path) {
|
|
140401
|
-
if (!
|
|
140778
|
+
if (!isRec3(s)) return [{ path, reason: "must be an object" }];
|
|
140402
140779
|
const id = typeof s.id === "string" ? s.id : void 0;
|
|
140403
140780
|
const out = [];
|
|
140404
140781
|
const at = (k4) => ({ id, path: `${path}.${k4}` });
|
|
@@ -140423,7 +140800,7 @@ function schemaIssues(schema, limit = 50) {
|
|
|
140423
140800
|
if (!Array.isArray(s.pages)) out.push({ path: "pages", reason: "must be a list" });
|
|
140424
140801
|
pages.forEach((p4, i2) => {
|
|
140425
140802
|
const path = `pages.${String(i2)}`;
|
|
140426
|
-
if (!
|
|
140803
|
+
if (!isRec3(p4)) {
|
|
140427
140804
|
out.push({ path, reason: "must be an object" });
|
|
140428
140805
|
return;
|
|
140429
140806
|
}
|
|
@@ -140524,7 +140901,9 @@ var OP_NAMES = [
|
|
|
140524
140901
|
"set_orientation",
|
|
140525
140902
|
"set_header",
|
|
140526
140903
|
"set_headers",
|
|
140527
|
-
"reorder"
|
|
140904
|
+
"reorder",
|
|
140905
|
+
"set_section",
|
|
140906
|
+
"set_section_title"
|
|
140528
140907
|
];
|
|
140529
140908
|
function placeBox(fs, bb, nb) {
|
|
140530
140909
|
if (nb.w <= 0 || nb.h <= 0) bad("the box must have a positive width and height");
|
|
@@ -140550,25 +140929,25 @@ function sectionIds(d, page) {
|
|
|
140550
140929
|
return out;
|
|
140551
140930
|
}
|
|
140552
140931
|
function typeAndFormat(t, prev) {
|
|
140553
|
-
const prevFmt =
|
|
140554
|
-
const
|
|
140932
|
+
const prevFmt = isRec2(prev?.format) ? prev.format : {};
|
|
140933
|
+
const keep2 = (keys) => Object.fromEntries(keys.filter((k4) => prevFmt[k4] != null).map((k4) => [k4, prevFmt[k4]]));
|
|
140555
140934
|
switch (t) {
|
|
140556
140935
|
case "text":
|
|
140557
140936
|
return {
|
|
140558
140937
|
type: "text",
|
|
140559
|
-
format: { ...
|
|
140938
|
+
format: { ...keep2(["font_size", "text_anchor", "uppercase"]), variant: "any" }
|
|
140560
140939
|
};
|
|
140561
140940
|
case "multiline":
|
|
140562
140941
|
return {
|
|
140563
140942
|
type: "text",
|
|
140564
|
-
format: { ...
|
|
140943
|
+
format: { ...keep2(["font_size", "uppercase"]), variant: "multiline" }
|
|
140565
140944
|
};
|
|
140566
140945
|
case "date":
|
|
140567
140946
|
return {
|
|
140568
140947
|
type: "date",
|
|
140569
140948
|
format: {
|
|
140570
140949
|
date_format: typeof prevFmt.date_format === "string" ? prevFmt.date_format : "DD/MM/YYYY",
|
|
140571
|
-
...
|
|
140950
|
+
...keep2(["font_size", "text_anchor"])
|
|
140572
140951
|
}
|
|
140573
140952
|
};
|
|
140574
140953
|
case "checkbox":
|
|
@@ -140596,7 +140975,7 @@ function entryType(d, id) {
|
|
|
140596
140975
|
}
|
|
140597
140976
|
const f3 = d.field.get(id);
|
|
140598
140977
|
if (!f3) return "?";
|
|
140599
|
-
if (f3.type === "text" &&
|
|
140978
|
+
if (f3.type === "text" && isRec2(f3.format) && f3.format.variant === "multiline") return "multiline";
|
|
140600
140979
|
if ((f3.type === "text" || f3.type === "date") && Array.isArray(f3.cells) && f3.cells.length > 1)
|
|
140601
140980
|
return "comb";
|
|
140602
140981
|
return String(f3.type);
|
|
@@ -140644,7 +141023,7 @@ function checkSnapTarget(g, op) {
|
|
|
140644
141023
|
if (op.snap !== void 0 && g) bad("snap applies to a single field, not a group");
|
|
140645
141024
|
}
|
|
140646
141025
|
function applyOne(d, op, warnings, opts) {
|
|
140647
|
-
if (!
|
|
141026
|
+
if (!isRec2(op) || typeof op.op !== "string") return bad("each op needs an `op` name");
|
|
140648
141027
|
const raw = op;
|
|
140649
141028
|
const table = (r) => r;
|
|
140650
141029
|
switch (op.op) {
|
|
@@ -140802,29 +141181,36 @@ function applyOne(d, op, warnings, opts) {
|
|
|
140802
141181
|
}
|
|
140803
141182
|
case "set_format": {
|
|
140804
141183
|
const { f: f3, g } = needId(d, op.id);
|
|
140805
|
-
if (!
|
|
141184
|
+
if (!isRec2(op.format)) return bad("format must be an object");
|
|
140806
141185
|
if (g) {
|
|
140807
141186
|
if (g.kind !== "comb") {
|
|
140808
141187
|
return bad(
|
|
140809
141188
|
g.kind === "table" ? "a table's formats live on its cells: set_format a cell (it applies to the cell's whole column or row), or set_column_type" : "set_format applies to a field, or to a comb group (cell_type, date_format)"
|
|
140810
141189
|
);
|
|
140811
141190
|
}
|
|
141191
|
+
const comb = g;
|
|
141192
|
+
const dated = op.format.date_format !== void 0 && op.format.date_format !== null;
|
|
140812
141193
|
for (const [k4, v] of Object.entries(op.format)) {
|
|
140813
141194
|
if (k4 !== "cell_type" && k4 !== "date_format")
|
|
140814
141195
|
bad(`a comb group's format keys are cell_type and date_format, not ${k4}`);
|
|
140815
141196
|
if (k4 === "date_format" && v !== null) checkCombDateFormat(d, g, v);
|
|
140816
|
-
if (v === null) delete
|
|
140817
|
-
else
|
|
141197
|
+
if (v === null) delete comb[k4];
|
|
141198
|
+
else comb[k4] = v;
|
|
140818
141199
|
}
|
|
140819
|
-
|
|
141200
|
+
if (dated) {
|
|
141201
|
+
if (op.format.cell_type !== void 0 && op.format.cell_type !== "date")
|
|
141202
|
+
bad('date_format needs cell_type "date": leave cell_type out, or pass "date"');
|
|
141203
|
+
comb.cell_type = "date";
|
|
141204
|
+
}
|
|
141205
|
+
return { touched: [g.id], comb: g.id };
|
|
140820
141206
|
}
|
|
140821
141207
|
const fld = f3;
|
|
140822
141208
|
const own2 = d.owner.get(fld.id);
|
|
140823
141209
|
if (own2 && (own2.kind === "comb" || own2.kind === "choice")) {
|
|
140824
141210
|
bad(`this is a member of ${String(own2.kind)} group ${own2.id}, whose kind fixes its format`);
|
|
140825
141211
|
}
|
|
140826
|
-
if (fld.type === "signature" && !
|
|
140827
|
-
const next = { ...
|
|
141212
|
+
if (fld.type === "signature" && !isRec2(fld.format)) fld.format = {};
|
|
141213
|
+
const next = { ...isRec2(fld.format) ? fld.format : {} };
|
|
140828
141214
|
for (const [k4, v] of Object.entries(op.format)) {
|
|
140829
141215
|
if (k4 === "date_format" && v !== null && !DATE_FORMATS.includes(v))
|
|
140830
141216
|
bad(`date_format must be one of ${DATE_FORMATS.join(", ")}`);
|
|
@@ -140839,7 +141225,7 @@ function applyOne(d, op, warnings, opts) {
|
|
|
140839
141225
|
delete fld.format;
|
|
140840
141226
|
else fld.format = next;
|
|
140841
141227
|
if (own2?.kind === "table") {
|
|
140842
|
-
applyFormatToLine(d, own2, fld,
|
|
141228
|
+
applyFormatToLine(d, own2, fld, isRec2(fld.format) ? fld.format : void 0, warnings);
|
|
140843
141229
|
return { touched: [fld.id, own2.id], tables: [own2.id] };
|
|
140844
141230
|
}
|
|
140845
141231
|
return { touched: [fld.id] };
|
|
@@ -140926,7 +141312,7 @@ function applyOne(d, op, warnings, opts) {
|
|
|
140926
141312
|
});
|
|
140927
141313
|
} else {
|
|
140928
141314
|
for (const o of op.options) {
|
|
140929
|
-
if (!
|
|
141315
|
+
if (!isRec2(o) || typeof o.id !== "string" || typeof o.label !== "string") {
|
|
140930
141316
|
return bad("options must be all strings, or all {id, label} pairs");
|
|
140931
141317
|
}
|
|
140932
141318
|
const oid = resolveId(d, o.id, ["field"], "option id");
|
|
@@ -140947,7 +141333,7 @@ function applyOne(d, op, warnings, opts) {
|
|
|
140947
141333
|
const n = ms.length;
|
|
140948
141334
|
if (op.index !== void 0 && (!Number.isInteger(op.index) || op.index < 0 || op.index > n))
|
|
140949
141335
|
return bad(`index must be 0\u2013${String(n)} (${String(n)} = last)`);
|
|
140950
|
-
const sib = ms.find((m) => m.type === "checkbox" &&
|
|
141336
|
+
const sib = ms.find((m) => m.type === "checkbox" && isRec2(m.format));
|
|
140951
141337
|
const f3 = {
|
|
140952
141338
|
id: randomUUID2(),
|
|
140953
141339
|
label: op.label.trim(),
|
|
@@ -141027,29 +141413,31 @@ function applyOne(d, op, warnings, opts) {
|
|
|
141027
141413
|
case "set_headers":
|
|
141028
141414
|
return table(setHeaders(d, raw, warnings));
|
|
141029
141415
|
case "reorder":
|
|
141030
|
-
return reorder(d, raw);
|
|
141416
|
+
return reorder(d, raw, warnings);
|
|
141417
|
+
case "set_section":
|
|
141418
|
+
return setSection(d, raw);
|
|
141419
|
+
case "set_section_title":
|
|
141420
|
+
return setSectionTitle(d, raw);
|
|
141031
141421
|
default:
|
|
141032
141422
|
return bad(`unknown op "${String(op.op)}"; ops: ${OP_NAMES.join(", ")}`);
|
|
141033
141423
|
}
|
|
141034
141424
|
}
|
|
141035
|
-
function reorder(d, op) {
|
|
141425
|
+
function reorder(d, op, warnings) {
|
|
141036
141426
|
const page = pageOf(d, op.page);
|
|
141037
141427
|
if (!Array.isArray(op.ids) || op.ids.length === 0)
|
|
141038
141428
|
return bad("ids must list the entries to order");
|
|
141039
|
-
const
|
|
141429
|
+
const orderOf2 = (item) => typeof item.order === "number" ? item.order : void 0;
|
|
141430
|
+
const unordered = [];
|
|
141040
141431
|
for (const f3 of d.s.fields) {
|
|
141041
|
-
if (f3.page === page && !d.owner.has(f3.id))
|
|
141432
|
+
if (f3.page === page && !d.owner.has(f3.id))
|
|
141433
|
+
unordered.push({ ...boxEdges(f3), item: f3, order: orderOf2(f3) });
|
|
141042
141434
|
}
|
|
141043
141435
|
for (const g of d.s.groups) {
|
|
141044
141436
|
if (g.page !== page) continue;
|
|
141045
|
-
const
|
|
141046
|
-
if (
|
|
141437
|
+
const edges = membersEdges(members2(d, g));
|
|
141438
|
+
if (edges) unordered.push({ ...edges, item: g, order: orderOf2(g) });
|
|
141047
141439
|
}
|
|
141048
|
-
const
|
|
141049
|
-
const hasOrder = blocks.some((b) => orderOf(b) !== void 0);
|
|
141050
|
-
blocks.sort(
|
|
141051
|
-
(a, b) => (hasOrder ? (orderOf(a) ?? Infinity) - (orderOf(b) ?? Infinity) || 0 : 0) || a.top - b.top || a.left - b.left
|
|
141052
|
-
);
|
|
141440
|
+
const blocks = orderBlocks(unordered);
|
|
141053
141441
|
const byId = new Map(blocks.map((b) => [b.item.id, b]));
|
|
141054
141442
|
const want = op.ids.map((raw) => {
|
|
141055
141443
|
const id = resolveId(d, raw, ["field", "group"], "ids entry");
|
|
@@ -141070,10 +141458,132 @@ function reorder(d, op) {
|
|
|
141070
141458
|
...want.map((id) => byId.get(id)),
|
|
141071
141459
|
...rest.slice(before)
|
|
141072
141460
|
];
|
|
141461
|
+
const was = listedOrder(d, page);
|
|
141073
141462
|
next.forEach((b, i2) => {
|
|
141074
141463
|
b.item.order = i2;
|
|
141075
141464
|
});
|
|
141076
|
-
|
|
141465
|
+
const listed = listedOrder(d, page);
|
|
141466
|
+
const from = listed.indexOf(want[0]);
|
|
141467
|
+
const asAsked = from !== -1 && want.every((id, k4) => listed[from + k4] === id);
|
|
141468
|
+
if (!asAsked) {
|
|
141469
|
+
const why = `a section is listed as one run, standing where its first entry stands, and these ids span several (${sectionsOf(d, page, want)})`;
|
|
141470
|
+
const fix = "set_section {ids, section_id} moves an entry to another section; inside one section any order works";
|
|
141471
|
+
if (listed.every((id, k4) => id === was[k4])) {
|
|
141472
|
+
return bad(
|
|
141473
|
+
`this order would change nothing in the listing: ${why}. ${fix[0].toUpperCase()}${fix.slice(1)}`
|
|
141474
|
+
);
|
|
141475
|
+
}
|
|
141476
|
+
warnings.push(
|
|
141477
|
+
`reorder on page ${String(page)}: the ids are not all listed in the order asked, because ${why}. diff_summary.ops shows the order now listed; ${fix}`
|
|
141478
|
+
);
|
|
141479
|
+
}
|
|
141480
|
+
return { touched: want, order: listed };
|
|
141481
|
+
}
|
|
141482
|
+
function pageSections(d, page) {
|
|
141483
|
+
const pg = d.s.pages.find((p4) => p4.page === page);
|
|
141484
|
+
return (Array.isArray(pg?.sections) ? pg.sections : []).flatMap(
|
|
141485
|
+
(s) => isRec2(s) && typeof s.id === "string" ? [{ id: s.id, title: typeof s.title === "string" ? s.title : "" }] : []
|
|
141486
|
+
);
|
|
141487
|
+
}
|
|
141488
|
+
function readingMembers(d, g) {
|
|
141489
|
+
if (!Array.isArray(g.members)) return [];
|
|
141490
|
+
const byId = buildFieldById(d.s.fields);
|
|
141491
|
+
return resolveMembers(
|
|
141492
|
+
g,
|
|
141493
|
+
byId
|
|
141494
|
+
);
|
|
141495
|
+
}
|
|
141496
|
+
function listedOrder(d, page) {
|
|
141497
|
+
const info = d.s.pages.find((p4) => p4.page === page);
|
|
141498
|
+
const fields = d.s.fields.filter((f3) => f3.page === page && !d.owner.has(f3.id));
|
|
141499
|
+
const groups = d.s.groups.filter((g) => g.page === page && Array.isArray(g.members));
|
|
141500
|
+
const byId = buildFieldById(d.s.fields);
|
|
141501
|
+
const layout = buildPageItems(
|
|
141502
|
+
{ ...info, page, sections: pageSections(d, page) },
|
|
141503
|
+
fields,
|
|
141504
|
+
groups,
|
|
141505
|
+
(g) => resolveMembers(g, byId)
|
|
141506
|
+
);
|
|
141507
|
+
return layout.items.flatMap(
|
|
141508
|
+
(it) => it.kind === "header" ? [] : [it.kind === "field" ? it.field.id : it.group.id]
|
|
141509
|
+
);
|
|
141510
|
+
}
|
|
141511
|
+
function sectionOf(d, page, id) {
|
|
141512
|
+
const g = d.group.get(id);
|
|
141513
|
+
const sid = g ? readingMembers(d, g)[0]?.section_id : d.field.get(id)?.section_id;
|
|
141514
|
+
if (typeof sid !== "string") return null;
|
|
141515
|
+
return pageSections(d, page).find((s) => s.id === sid) ?? null;
|
|
141516
|
+
}
|
|
141517
|
+
function sectionsOf(d, page, ids) {
|
|
141518
|
+
const by = /* @__PURE__ */ new Map();
|
|
141519
|
+
for (const id of ids) {
|
|
141520
|
+
const sec = sectionOf(d, page, id);
|
|
141521
|
+
const key = sec ? sec.id : "";
|
|
141522
|
+
const row = by.get(key) ?? {
|
|
141523
|
+
name: sec ? `section ${JSON.stringify(sec.title)} (${sec.id})` : "no section",
|
|
141524
|
+
ids: []
|
|
141525
|
+
};
|
|
141526
|
+
row.ids.push(id);
|
|
141527
|
+
by.set(key, row);
|
|
141528
|
+
}
|
|
141529
|
+
return [...by.values()].map(
|
|
141530
|
+
(r) => `${r.name}: ${r.ids.slice(0, 3).join(", ")}${r.ids.length > 3 ? ` and ${String(r.ids.length - 3)} more` : ""}`
|
|
141531
|
+
).join("; ");
|
|
141532
|
+
}
|
|
141533
|
+
function setSection(d, op) {
|
|
141534
|
+
if (!Array.isArray(op.ids) || op.ids.length === 0)
|
|
141535
|
+
return bad("ids must list the fields or groups to move");
|
|
141536
|
+
const ids = [
|
|
141537
|
+
...new Set(
|
|
141538
|
+
op.ids.map((raw) => {
|
|
141539
|
+
const id = resolveId(d, raw, ["field", "group"], "ids entry");
|
|
141540
|
+
const own2 = d.owner.get(id);
|
|
141541
|
+
if (own2) return bad(`${id} is an option/cell of group ${own2.id}; move the group`);
|
|
141542
|
+
return id;
|
|
141543
|
+
})
|
|
141544
|
+
)
|
|
141545
|
+
];
|
|
141546
|
+
const pageOfEntry = (id) => (d.group.get(id) ?? d.field.get(id))?.page;
|
|
141547
|
+
const page = pageOfEntry(ids[0]);
|
|
141548
|
+
if (typeof page !== "number" || ids.some((id) => pageOfEntry(id) !== page))
|
|
141549
|
+
return bad("all ids must be on one page: a section belongs to one page");
|
|
141550
|
+
let section = null;
|
|
141551
|
+
if (op.section_id === void 0) {
|
|
141552
|
+
return bad("section_id is required (null takes the entries out of every section)");
|
|
141553
|
+
}
|
|
141554
|
+
if (op.section_id !== null) {
|
|
141555
|
+
const sid = resolveId(d, op.section_id, ["section"], "section_id");
|
|
141556
|
+
const found = pageSections(d, page).find((s) => s.id === sid);
|
|
141557
|
+
if (!found) {
|
|
141558
|
+
const have = pageSections(d, page).map((s) => `${s.id} ${JSON.stringify(s.title)}`);
|
|
141559
|
+
return bad(
|
|
141560
|
+
`section_id ${sid} is not a section of page ${String(page)} (${have.length ? `its sections: ${have.join(", ")}` : "the page has no sections"})`
|
|
141561
|
+
);
|
|
141562
|
+
}
|
|
141563
|
+
section = found;
|
|
141564
|
+
}
|
|
141565
|
+
for (const id of ids) {
|
|
141566
|
+
const g = d.group.get(id);
|
|
141567
|
+
for (const f3 of g ? members2(d, g) : [d.field.get(id)]) {
|
|
141568
|
+
f3.section_id = section ? section.id : null;
|
|
141569
|
+
}
|
|
141570
|
+
}
|
|
141571
|
+
return { touched: ids, section };
|
|
141572
|
+
}
|
|
141573
|
+
function setSectionTitle(d, op) {
|
|
141574
|
+
const page = pageOf(d, op.page);
|
|
141575
|
+
const sid = resolveId(d, op.section_id, ["section"], "section_id");
|
|
141576
|
+
const pg = d.s.pages.find((p4) => p4.page === page);
|
|
141577
|
+
const sec = (Array.isArray(pg?.sections) ? pg.sections : []).find(
|
|
141578
|
+
(s) => isRec2(s) && s.id === sid
|
|
141579
|
+
);
|
|
141580
|
+
if (!sec) return bad(`section_id ${sid} is not a section of page ${String(page)}`);
|
|
141581
|
+
if (typeof op.title !== "string" || !op.title.trim())
|
|
141582
|
+
return bad("title is required: the heading the form prints over these fields");
|
|
141583
|
+
sec.title = op.title.trim();
|
|
141584
|
+
const issues = sectionIssues(sec, `section ${sid}`);
|
|
141585
|
+
if (issues.length) return bad(issues.map((i2) => `${i2.path} ${i2.reason}`).join("; "));
|
|
141586
|
+
return { touched: [], section: { id: sid, title: sec.title } };
|
|
141077
141587
|
}
|
|
141078
141588
|
function checkCombDateFormat(d, g, v) {
|
|
141079
141589
|
const slots = members2(d, g).reduce(
|
|
@@ -141086,6 +141596,18 @@ function checkCombDateFormat(d, g, v) {
|
|
|
141086
141596
|
);
|
|
141087
141597
|
}
|
|
141088
141598
|
}
|
|
141599
|
+
function combFormat(render, id) {
|
|
141600
|
+
try {
|
|
141601
|
+
const schema = normalizeSchema(
|
|
141602
|
+
structuredClone(render)
|
|
141603
|
+
);
|
|
141604
|
+
const e4 = agentEntries(schema).find((x2) => x2.id === id);
|
|
141605
|
+
if (e4?.kind !== "comb") return null;
|
|
141606
|
+
return { cell_type: e4.cellType, date_format: e4.dateFormat, max_length: e4.maxLength };
|
|
141607
|
+
} catch {
|
|
141608
|
+
return null;
|
|
141609
|
+
}
|
|
141610
|
+
}
|
|
141089
141611
|
function touchedIssues(d, ids, tables = []) {
|
|
141090
141612
|
const out = [];
|
|
141091
141613
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -141127,6 +141649,8 @@ function applyPass(render, ops, excluded, screen, opts) {
|
|
|
141127
141649
|
const touchedIds = /* @__PURE__ */ new Set();
|
|
141128
141650
|
const warnings = [];
|
|
141129
141651
|
const outcomes = [];
|
|
141652
|
+
const told = [];
|
|
141653
|
+
const confirmed = [];
|
|
141130
141654
|
let applied = 0;
|
|
141131
141655
|
ops.forEach((raw, i2) => {
|
|
141132
141656
|
const skip = excluded.get(i2);
|
|
@@ -141145,30 +141669,46 @@ function applyPass(render, ops, excluded, screen, opts) {
|
|
|
141145
141669
|
issues.slice(0, 5).map((x2) => `${x2.path} ${x2.reason}`).join("; ")
|
|
141146
141670
|
);
|
|
141147
141671
|
}
|
|
141672
|
+
let vouched = [];
|
|
141148
141673
|
if (screen) {
|
|
141149
141674
|
const changed = changedWording(cur, draft);
|
|
141150
|
-
const echo = changed.length ? screen(changed) :
|
|
141151
|
-
if (echo.
|
|
141152
|
-
|
|
141153
|
-
|
|
141154
|
-
)
|
|
141675
|
+
const echo = changed.length ? screen(changed) : null;
|
|
141676
|
+
if (echo?.refused.length) {
|
|
141677
|
+
const known = index(cur);
|
|
141678
|
+
const name2 = (id) => known.field.has(id) || known.group.has(id) || known.section.has(id) ? id : "the new entry";
|
|
141679
|
+
if (opts.confirmWording === true && echo.refused.every((x2) => x2.shown)) {
|
|
141680
|
+
vouched = echo.refused;
|
|
141681
|
+
w.push(
|
|
141682
|
+
`${describeWording(echo.refused, name2)}: matches an answer filled in this session; applied because confirm_wording says it is the form's own wording`
|
|
141683
|
+
);
|
|
141684
|
+
} else {
|
|
141685
|
+
told.push(...echo.refused);
|
|
141686
|
+
bad(
|
|
141687
|
+
`${describeWording(echo.refused, name2)} matches an answer filled in this session. Labels, descriptions, placeholders, options and formats hold the form's own wording, never an answer: answers go only in fill values. If this is the form's printed wording, send the op again with confirm_wording: true on the call`
|
|
141688
|
+
);
|
|
141689
|
+
}
|
|
141155
141690
|
}
|
|
141156
|
-
for (const id of echo
|
|
141691
|
+
for (const id of echo?.warn ?? []) {
|
|
141157
141692
|
w.push(
|
|
141158
|
-
`${describeWording(changed
|
|
141693
|
+
`${describeWording(changed.filter((x2) => x2.id === id))}: short wording that matches a short answer from this session (allowed under 4 characters); check it is the form's own text`
|
|
141159
141694
|
);
|
|
141160
141695
|
}
|
|
141161
141696
|
}
|
|
141162
141697
|
const name = raw.op;
|
|
141163
141698
|
const noop = JSON.stringify(draft) === JSON.stringify(cur);
|
|
141699
|
+
const comb = r.comb !== void 0 ? combFormat(draft, r.comb) : null;
|
|
141164
141700
|
outcomes.push({
|
|
141165
141701
|
op_index: i2,
|
|
141166
141702
|
op: name,
|
|
141167
141703
|
...r.snapped !== void 0 ? { snapped: r.snapped } : {},
|
|
141704
|
+
...comb ? { comb } : {},
|
|
141705
|
+
...r.order ? { order: r.order } : {},
|
|
141706
|
+
...r.section !== void 0 ? { section: r.section } : {},
|
|
141168
141707
|
...noop ? { noop: true } : {}
|
|
141169
141708
|
});
|
|
141170
141709
|
cur = draft;
|
|
141171
141710
|
applied++;
|
|
141711
|
+
confirmed.push(...vouched);
|
|
141172
141712
|
counts[name] = (counts[name] ?? 0) + 1;
|
|
141173
141713
|
if (r.added) added.push({ op_index: i2, id: r.added });
|
|
141174
141714
|
if (r.geometry) placed.set(i2, r.geometry);
|
|
@@ -141210,16 +141750,17 @@ function applyPass(render, ops, excluded, screen, opts) {
|
|
|
141210
141750
|
applied,
|
|
141211
141751
|
rejected,
|
|
141212
141752
|
diff_summary: { counts, added, deleted, touched, ops: outcomes },
|
|
141213
|
-
warnings
|
|
141753
|
+
warnings,
|
|
141754
|
+
screened: { told, confirmed }
|
|
141214
141755
|
},
|
|
141215
141756
|
offPage
|
|
141216
141757
|
};
|
|
141217
141758
|
}
|
|
141218
141759
|
function wordingOf(s) {
|
|
141219
141760
|
const out = [];
|
|
141220
|
-
const add = (id, prop, v) => {
|
|
141761
|
+
const add = (id, prop, v, page) => {
|
|
141221
141762
|
if (typeof v === "string" && v.trim() && typeof id === "string")
|
|
141222
|
-
out.push({ id, prop, text: v });
|
|
141763
|
+
out.push({ id, ...typeof page === "number" ? { page } : {}, prop, text: v });
|
|
141223
141764
|
};
|
|
141224
141765
|
const rec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2) ? x2 : null;
|
|
141225
141766
|
for (const f3 of s.fields) {
|
|
@@ -141239,26 +141780,36 @@ function wordingOf(s) {
|
|
|
141239
141780
|
for (const p4 of s.pages) {
|
|
141240
141781
|
const secs = Array.isArray(p4.sections) ? p4.sections : [];
|
|
141241
141782
|
for (const sec of secs)
|
|
141242
|
-
for (const k4 of ["title", "description", "placeholder"]) add(sec.id, k4, sec[k4]);
|
|
141783
|
+
for (const k4 of ["title", "description", "placeholder"]) add(sec.id, k4, sec[k4], p4.page);
|
|
141243
141784
|
}
|
|
141244
141785
|
return out;
|
|
141245
141786
|
}
|
|
141246
141787
|
function changedWording(prev, next) {
|
|
141247
|
-
const
|
|
141248
|
-
|
|
141788
|
+
const at = (w) => `${entryKey(w)}\0${w.prop}\0${w.text}`;
|
|
141789
|
+
const before = new Set(wordingOf(prev).map(at));
|
|
141790
|
+
return wordingOf(next).filter((w) => !before.has(at(w)));
|
|
141249
141791
|
}
|
|
141250
141792
|
function wordingChanges(changed) {
|
|
141251
141793
|
const by = /* @__PURE__ */ new Map();
|
|
141252
141794
|
for (const w of changed) {
|
|
141253
|
-
const
|
|
141254
|
-
|
|
141255
|
-
|
|
141256
|
-
|
|
141257
|
-
|
|
141795
|
+
const key = entryKey(w);
|
|
141796
|
+
const entry = by.get(key) ?? {
|
|
141797
|
+
id: w.id,
|
|
141798
|
+
...w.page === void 0 ? {} : { page: w.page },
|
|
141799
|
+
props: /* @__PURE__ */ new Set()
|
|
141800
|
+
};
|
|
141801
|
+
entry.props.add(w.prop);
|
|
141802
|
+
by.set(key, entry);
|
|
141803
|
+
}
|
|
141804
|
+
return [...by.values()].map(({ props, ...entry }) => ({
|
|
141805
|
+
...entry,
|
|
141806
|
+
properties: [...props].sort()
|
|
141807
|
+
}));
|
|
141258
141808
|
}
|
|
141259
|
-
function describeWording(
|
|
141260
|
-
|
|
141261
|
-
|
|
141809
|
+
function describeWording(wording, name = (id) => id) {
|
|
141810
|
+
return wordingChanges(wording).map(
|
|
141811
|
+
(c2) => `the ${c2.properties.join("/")} of ${name(c2.id)}${c2.page === void 0 ? "" : ` on page ${String(c2.page)}`}`
|
|
141812
|
+
).join(", ");
|
|
141262
141813
|
}
|
|
141263
141814
|
|
|
141264
141815
|
// src/render.ts
|
|
@@ -141339,14 +141890,14 @@ async function withPage(pdf, page, fn5) {
|
|
|
141339
141890
|
p4.wasmExports.free(ptr);
|
|
141340
141891
|
}
|
|
141341
141892
|
}
|
|
141342
|
-
async function renderPage(pdf, page, dpi, crop) {
|
|
141893
|
+
async function renderPage(pdf, page, dpi, crop, maxSide = MAX_SIDE_PX) {
|
|
141343
141894
|
return withPage(pdf, page, (m, pg, heap) => {
|
|
141344
141895
|
const wPt = m.FPDF_GetPageWidthF(pg);
|
|
141345
141896
|
const hPt = m.FPDF_GetPageHeightF(pg);
|
|
141346
141897
|
const c2 = crop ?? { x: 0, y: 0, w: 1, h: 1 };
|
|
141347
141898
|
let scale2 = dpi / 72;
|
|
141348
141899
|
const longest = Math.max(wPt * c2.w, hPt * c2.h) * scale2;
|
|
141349
|
-
if (longest >
|
|
141900
|
+
if (longest > maxSide) scale2 *= maxSide / longest;
|
|
141350
141901
|
const fullW = Math.max(1, Math.round(wPt * scale2));
|
|
141351
141902
|
const fullH = Math.max(1, Math.round(hPt * scale2));
|
|
141352
141903
|
const x0 = Math.round(c2.x * fullW);
|
|
@@ -141375,7 +141926,7 @@ async function renderPage(pdf, page, dpi, crop) {
|
|
|
141375
141926
|
const row = buf + y * stride;
|
|
141376
141927
|
rgba.set(h8.subarray(row, row + width * 4), y * width * 4);
|
|
141377
141928
|
}
|
|
141378
|
-
return { width, height, rgba };
|
|
141929
|
+
return { width, height, rgba, scale: scale2 };
|
|
141379
141930
|
} finally {
|
|
141380
141931
|
m.FPDFBitmap_Destroy(bmp);
|
|
141381
141932
|
}
|
|
@@ -141812,10 +142363,10 @@ var FONT_MIN_PT = 3;
|
|
|
141812
142363
|
var OVERLAP_MIN_CHARS = 2;
|
|
141813
142364
|
var CELL_TOL = 25e-4;
|
|
141814
142365
|
var BLANK_CHARS = /^[_.\-‐-―…·•‧⋯─━┄┅┈┉_]$/u;
|
|
141815
|
-
var multiline = (f3) =>
|
|
142366
|
+
var multiline = (f3) => isRec2(f3.format) && f3.format.variant === "multiline";
|
|
141816
142367
|
var singleLine = (f3) => f3.type === "text" && !multiline(f3) || f3.type === "date" && !(f3.cells && f3.cells.length > 1);
|
|
141817
142368
|
function pinned(f3) {
|
|
141818
|
-
const s =
|
|
142369
|
+
const s = isRec2(f3.format) ? f3.format.font_size : void 0;
|
|
141819
142370
|
return typeof s === "number" ? s : null;
|
|
141820
142371
|
}
|
|
141821
142372
|
function stampedSize(f3, pageHeightPt) {
|
|
@@ -141941,7 +142492,9 @@ async function placementWarnings2(pdf, render, ids, opts = {}) {
|
|
|
141941
142492
|
}
|
|
141942
142493
|
|
|
141943
142494
|
// src/preview.ts
|
|
142495
|
+
var OVERLAYS = ["full", "outline", "none"];
|
|
141944
142496
|
var MAX_ZOOM = 8;
|
|
142497
|
+
var PREVIEW_MAX_SIDE_PX = 2e3;
|
|
141945
142498
|
var r4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
141946
142499
|
function boxOf(fs) {
|
|
141947
142500
|
if (!fs.length) return null;
|
|
@@ -141952,6 +142505,16 @@ function boxOf(fs) {
|
|
|
141952
142505
|
return { x: r4(x0 / 100), y: r4(y0 / 100), w: r4((x1 - x0) / 100), h: r4((y1 - y0) / 100) };
|
|
141953
142506
|
}
|
|
141954
142507
|
var topId2 = (id) => id.split(/[.[]/, 1)[0];
|
|
142508
|
+
function idPages(entries) {
|
|
142509
|
+
const pages = /* @__PURE__ */ new Map();
|
|
142510
|
+
for (const e4 of entries) {
|
|
142511
|
+
const page = e4.kind === "field" ? e4.field.page : e4.group.page;
|
|
142512
|
+
pages.set(e4.id, page);
|
|
142513
|
+
const inner = e4.kind === "choice" ? e4.options : e4.kind === "table" ? e4.cells : e4.kind === "table_rows" ? e4.columns : [];
|
|
142514
|
+
for (const x2 of inner) if (!pages.has(x2.id)) pages.set(x2.id, page);
|
|
142515
|
+
}
|
|
142516
|
+
return pages;
|
|
142517
|
+
}
|
|
141955
142518
|
function legendRows(render, page) {
|
|
141956
142519
|
const schema = normalizeSchema(render);
|
|
141957
142520
|
const entries = agentEntries(schema);
|
|
@@ -142087,6 +142650,7 @@ function legendChanges(before, now) {
|
|
|
142087
142650
|
}
|
|
142088
142651
|
async function previewPage(a) {
|
|
142089
142652
|
const dpi = Math.min(MAX_DPI, Math.max(MIN_DPI, Math.round(a.dpi ?? DEFAULT_DPI)));
|
|
142653
|
+
const overlay = a.overlay ?? "full";
|
|
142090
142654
|
const schema = normalizeSchema(a.render);
|
|
142091
142655
|
const pages = schema.pages.map((p4) => p4.page);
|
|
142092
142656
|
if (!pages.includes(a.page)) {
|
|
@@ -142104,27 +142668,41 @@ async function previewPage(a) {
|
|
|
142104
142668
|
branding: null,
|
|
142105
142669
|
logoBytes: null,
|
|
142106
142670
|
loadImage,
|
|
142107
|
-
|
|
142671
|
+
// "none" shows the stamped PDF itself: no check copy is built, so none can fail.
|
|
142672
|
+
check: overlay !== "none",
|
|
142108
142673
|
// preview_page's JSON carries the colour key; the banner would only
|
|
142109
142674
|
// cover the top of page 1.
|
|
142110
|
-
checkBanner: false
|
|
142675
|
+
checkBanner: false,
|
|
142676
|
+
checkTags: overlay === "full"
|
|
142111
142677
|
});
|
|
142112
142678
|
} catch (e4) {
|
|
142113
142679
|
if (isPasswordError(e4)) throw passwordError();
|
|
142114
142680
|
if (isFillForbiddenError(e4)) throw fillForbiddenError();
|
|
142115
142681
|
throw e4;
|
|
142116
142682
|
}
|
|
142117
|
-
|
|
142683
|
+
const shown = overlay === "none" ? res.pdf : res.checkPdf;
|
|
142684
|
+
if (!shown) {
|
|
142685
|
+
const err2 = res.checkError;
|
|
142686
|
+
throw new ToolError("check_copy_failed", checkCopyFailed(err2), {
|
|
142687
|
+
hint: err2?.id ? `Fix or remove ${err2.id} with edit_template, then preview again.` : "Preview again; if it persists, fill_form / fill_template still write the filled PDF, without a check copy.",
|
|
142688
|
+
details: { stage: err2?.stage, id: err2?.id }
|
|
142689
|
+
});
|
|
142690
|
+
}
|
|
142118
142691
|
const legend = legendRows(a.render, a.page);
|
|
142119
142692
|
const ids = new Set(legend.map((r) => r.id));
|
|
142120
|
-
const
|
|
142693
|
+
const where = idPages(agentEntries(schema));
|
|
142694
|
+
const skipped = res.skipped.filter((s) => {
|
|
142695
|
+
const top = topId2(s.id);
|
|
142696
|
+
const page = where.get(top);
|
|
142697
|
+
return ids.has(top) || page === void 0 || page === a.page;
|
|
142698
|
+
});
|
|
142121
142699
|
const warnings = res.warnings.filter((w) => {
|
|
142122
142700
|
const head = w.slice(0, Math.max(0, w.indexOf(":")));
|
|
142123
142701
|
return head.split(" and ").some((id) => ids.has(topId2(id.trim())));
|
|
142124
142702
|
});
|
|
142125
142703
|
const zoom = a.crop ? Math.min(MAX_ZOOM, Math.max(1, a.zoom ?? 2)) : 1;
|
|
142126
|
-
const raster = await renderPage(
|
|
142127
|
-
const
|
|
142704
|
+
const raster = await renderPage(shown, a.page, dpi * zoom, a.crop, PREVIEW_MAX_SIDE_PX);
|
|
142705
|
+
const listed = a.crop ? legend.filter((r) => {
|
|
142128
142706
|
const c2 = a.crop;
|
|
142129
142707
|
return r.box.x < c2.x + c2.w && c2.x < r.box.x + r.box.w && r.box.y < c2.y + c2.h && c2.y < r.box.y + r.box.h;
|
|
142130
142708
|
}) : legend;
|
|
@@ -142132,8 +142710,10 @@ async function previewPage(a) {
|
|
|
142132
142710
|
png: encodePng2(raster),
|
|
142133
142711
|
width: raster.width,
|
|
142134
142712
|
height: raster.height,
|
|
142135
|
-
|
|
142136
|
-
|
|
142713
|
+
// What was rendered, not what was asked: a capped image is coarser, and
|
|
142714
|
+
// an agent reads sizes off it.
|
|
142715
|
+
dpi: Math.round(raster.scale * 72 * 10) / 10,
|
|
142716
|
+
legend: listed,
|
|
142137
142717
|
filled: res.filled,
|
|
142138
142718
|
skipped,
|
|
142139
142719
|
warnings
|
|
@@ -142698,6 +143278,52 @@ async function sweepCache(cache3, now) {
|
|
|
142698
143278
|
return removed;
|
|
142699
143279
|
}
|
|
142700
143280
|
|
|
143281
|
+
// src/values.ts
|
|
143282
|
+
function expandKeys(values2, pool3) {
|
|
143283
|
+
const given = Object.keys(values2);
|
|
143284
|
+
const taken = /* @__PURE__ */ new Set();
|
|
143285
|
+
const pairs = [];
|
|
143286
|
+
for (const key of given) {
|
|
143287
|
+
let id = key;
|
|
143288
|
+
if (!pool3.includes(key) && key.length >= MIN_ID_PREFIX) {
|
|
143289
|
+
const hits2 = pool3.filter((p4) => p4.startsWith(key));
|
|
143290
|
+
if (hits2.length === 1 && !given.includes(hits2[0]) && !taken.has(hits2[0])) id = hits2[0];
|
|
143291
|
+
}
|
|
143292
|
+
taken.add(id);
|
|
143293
|
+
pairs.push([id, values2[key]]);
|
|
143294
|
+
}
|
|
143295
|
+
return Object.fromEntries(pairs);
|
|
143296
|
+
}
|
|
143297
|
+
function expandNested(e4, v) {
|
|
143298
|
+
if (e4.kind === "table" && isRec2(v)) {
|
|
143299
|
+
return expandKeys(
|
|
143300
|
+
v,
|
|
143301
|
+
e4.cells.map((c2) => c2.id)
|
|
143302
|
+
);
|
|
143303
|
+
}
|
|
143304
|
+
if (e4.kind === "table_rows" && Array.isArray(v)) {
|
|
143305
|
+
const columns = e4.columns.map((c2) => c2.id);
|
|
143306
|
+
return v.map((row) => isRec2(row) ? expandKeys(row, columns) : row);
|
|
143307
|
+
}
|
|
143308
|
+
return v;
|
|
143309
|
+
}
|
|
143310
|
+
function expandValueKeys(render, values2) {
|
|
143311
|
+
let entries;
|
|
143312
|
+
try {
|
|
143313
|
+
entries = agentEntries(normalizeSchema(render));
|
|
143314
|
+
} catch {
|
|
143315
|
+
return values2;
|
|
143316
|
+
}
|
|
143317
|
+
const byId = new Map(entries.map((e4) => [e4.id, e4]));
|
|
143318
|
+
const top = expandKeys(values2, [...byId.keys()]);
|
|
143319
|
+
return Object.fromEntries(
|
|
143320
|
+
Object.entries(top).map(([id, v]) => {
|
|
143321
|
+
const e4 = byId.get(id);
|
|
143322
|
+
return [id, e4 ? expandNested(e4, v) : v];
|
|
143323
|
+
})
|
|
143324
|
+
);
|
|
143325
|
+
}
|
|
143326
|
+
|
|
142701
143327
|
// src/server.ts
|
|
142702
143328
|
var VALUE_SHAPES = `values maps field ids to: text/multiline = string, stamped exactly as given (a date in a text field goes in the format its label or placeholder asks, e.g. "mmddyyyy" \u2192 "10042026"); date = "YYYY-MM-DD", printed in the field's date_format (shown on the field, e.g. DD/MM/YYYY); comb = string, one character per cell; checkbox = boolean; radio = option id or label, or the bare option text ("No" for "Smoke? No"); multiselect = array of those; table = {cellId: value}; table_rows = [{columnId: value}, \u2026]; signature = {"image_path": "/abs/sig.png"} (PNG or JPG). A signature with signing_requirement "external" is signed outside fenfill (on paper or by e-sign) and stays blank.`;
|
|
142703
143329
|
var INSTRUCTIONS = [
|
|
@@ -142710,7 +143336,9 @@ var INSTRUCTIONS = [
|
|
|
142710
143336
|
].join(" ");
|
|
142711
143337
|
var TEMPLATE_ID = external_exports.string().regex(/^[A-Za-z0-9-]{1,64}$/, "a fenfill template id").describe("Template id (from list_templates).");
|
|
142712
143338
|
var PAGES = external_exports.string().max(2e3).optional().describe('1-based pages, e.g. "1-3,5". Omit for all.');
|
|
142713
|
-
var VALUES = external_exports.record(external_exports.string(), external_exports.unknown()).describe(
|
|
143339
|
+
var VALUES = external_exports.record(external_exports.string(), external_exports.unknown()).describe(
|
|
143340
|
+
"Field id \u2192 value (see description). An id may be shortened to an unambiguous prefix of 6+ characters (the preview tags), a table's cell or column id inside its value too; a key that matches nothing is listed in skipped."
|
|
143341
|
+
);
|
|
142714
143342
|
var OUTPUT_PATH = external_exports.string().min(1).max(4096).describe("Absolute path (or ~/\u2026) of the new .pdf to write.");
|
|
142715
143343
|
var OVERWRITE = external_exports.boolean().optional().describe("Replace output_path if it exists.");
|
|
142716
143344
|
var TARGET_PATH = external_exports.string().min(1).max(4096).optional().describe(
|
|
@@ -142757,7 +143385,14 @@ async function refreshSavedAnalyses(cache3, templateId, render, saved, who) {
|
|
|
142757
143385
|
const agent = saved.result && Array.isArray(saved.result.fields) ? saved.result : e4.agent;
|
|
142758
143386
|
await cache3.saveAnalysis({
|
|
142759
143387
|
...e4,
|
|
142760
|
-
|
|
143388
|
+
// The cached pages keep their own images; the sections are the saved ones.
|
|
143389
|
+
render: {
|
|
143390
|
+
...render,
|
|
143391
|
+
pages: e4.render.pages.map((p4) => {
|
|
143392
|
+
const saved2 = render.pages.find((x2) => x2.page === p4.page);
|
|
143393
|
+
return saved2?.sections !== void 0 ? { ...p4, sections: saved2.sections } : p4;
|
|
143394
|
+
})
|
|
143395
|
+
},
|
|
142761
143396
|
agent: { ...agent, template_id: templateId, updated_at: saved.updated_at }
|
|
142762
143397
|
}).catch(() => void 0);
|
|
142763
143398
|
}
|
|
@@ -142814,6 +143449,19 @@ function createFenfillServer(opts = {}) {
|
|
|
142814
143449
|
};
|
|
142815
143450
|
maybeSweep();
|
|
142816
143451
|
const echo = new EchoGuard();
|
|
143452
|
+
const takenInputs = /* @__PURE__ */ new WeakSet();
|
|
143453
|
+
const taken = (render, values2) => {
|
|
143454
|
+
takenInputs.add(values2);
|
|
143455
|
+
const answers = expandValueKeys(render, values2);
|
|
143456
|
+
echo.remember(render, answers);
|
|
143457
|
+
return answers;
|
|
143458
|
+
};
|
|
143459
|
+
const untaken = (args) => {
|
|
143460
|
+
const given = args?.values;
|
|
143461
|
+
if (typeof given !== "object" || given === null || Array.isArray(given)) return;
|
|
143462
|
+
if (!takenInputs.has(given)) echo.rememberUnread(given);
|
|
143463
|
+
};
|
|
143464
|
+
const listedForeign = /* @__PURE__ */ new Map();
|
|
142817
143465
|
const lastLegend = /* @__PURE__ */ new Map();
|
|
142818
143466
|
const fileForm = async (path) => {
|
|
142819
143467
|
const { abs: abs2, bytes } = await readInputPdf(path);
|
|
@@ -142889,6 +143537,7 @@ function createFenfillServer(opts = {}) {
|
|
|
142889
143537
|
log("info", "tool ok", { tool: name, ms: Date.now() - t0 });
|
|
142890
143538
|
return ok(result);
|
|
142891
143539
|
} catch (e4) {
|
|
143540
|
+
untaken(args);
|
|
142892
143541
|
const err2 = asToolError(e4);
|
|
142893
143542
|
log("warn", "tool failed", { tool: name, code: err2.code, ms: Date.now() - t0 });
|
|
142894
143543
|
return fail(err2);
|
|
@@ -142989,11 +143638,11 @@ function createFenfillServer(opts = {}) {
|
|
|
142989
143638
|
const templateId = soleTemplateId(merged, null, await identity2(false));
|
|
142990
143639
|
const wc = await fileEdits({ sha, templateId });
|
|
142991
143640
|
const render = wc ? wc.render : merged.render;
|
|
142992
|
-
|
|
143641
|
+
const answers = taken(render, values2);
|
|
142993
143642
|
const res = await stampAndWrite({
|
|
142994
143643
|
pdfBytes: bytes,
|
|
142995
143644
|
render,
|
|
142996
|
-
agentInput:
|
|
143645
|
+
agentInput: answers,
|
|
142997
143646
|
fonts: loadFonts(opts.fontsDir),
|
|
142998
143647
|
branding: null,
|
|
142999
143648
|
logoBytes: null,
|
|
@@ -143051,7 +143700,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143051
143700
|
}
|
|
143052
143701
|
render = tpl.render;
|
|
143053
143702
|
}
|
|
143054
|
-
|
|
143703
|
+
const answers = taken(render, values2);
|
|
143055
143704
|
const { body: account } = await client.getJson("/account", { signal, deadline });
|
|
143056
143705
|
const blank = await templateBlank(template_id, signal, deadline);
|
|
143057
143706
|
let logo = null;
|
|
@@ -143067,7 +143716,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143067
143716
|
const res = await stampAndWrite({
|
|
143068
143717
|
pdfBytes: blank,
|
|
143069
143718
|
render,
|
|
143070
|
-
agentInput:
|
|
143719
|
+
agentInput: answers,
|
|
143071
143720
|
fonts: loadFonts(opts.fontsDir),
|
|
143072
143721
|
branding,
|
|
143073
143722
|
logoBytes: logo ? toArrayBuffer(logo) : null,
|
|
@@ -143183,24 +143832,31 @@ function createFenfillServer(opts = {}) {
|
|
|
143183
143832
|
"preview_page",
|
|
143184
143833
|
{
|
|
143185
143834
|
title: "Preview a page with its field boxes",
|
|
143186
|
-
description: `Render one page as a PNG with every field box outlined (green filled, orange warning, red skipped, grey empty) and tagged with its id prefix, plus a legend: tag \u2192 id, type, label, box (page fractions, top-left origin, as edit_template uses), format and font_pt; options, table cells (row, col) and table_rows columns with their own boxes. placement_warnings ([] = checked, clean) covers every box shown: much larger text than the page's other fields, a box over printed text (not on scans), a box past its ruled cell. Use it as a read-only placement audit. values (see that parameter) stamp test values. crop + zoom magnify a region; legend: "changed" lists only what changed since your last preview of the page (moved_into: fields now inside a table or group). It renders the local working copy (edit_template) when one exists, else the saved or analyzed version (edited: true/false). Rendered on this machine: nothing is written to disk and the values are never sent anywhere.`,
|
|
143835
|
+
description: `Render one page as a PNG with every field box outlined (green filled, orange warning, red skipped, grey empty) and tagged with its id prefix, plus a legend: tag \u2192 id, type, label, box (page fractions, top-left origin, as edit_template uses), format and font_pt; options, table cells (row, col) and table_rows columns with their own boxes. placement_warnings ([] = checked, clean) covers every box shown: much larger text than the page's other fields, a box over printed text (not on scans), a box past its ruled cell. Use it as a read-only placement audit. values (see that parameter) stamp test values. overlay: "outline" drops the tags, "none" shows the page with only the stamped values (to read print under a tag, or after check_copy_failed). crop + zoom magnify a region; legend: "changed" lists only what changed since your last preview of the page (moved_into: fields now inside a table or group). It renders the local working copy (edit_template) when one exists, else the saved or analyzed version (edited: true/false). Rendered on this machine: nothing is written to disk and the values are never sent anywhere.`,
|
|
143187
143836
|
inputSchema: {
|
|
143188
143837
|
path: TARGET_PATH,
|
|
143189
143838
|
template_id: TARGET_TEMPLATE,
|
|
143190
143839
|
page: external_exports.number().int().min(1).max(1e5).describe("1-based page number."),
|
|
143191
|
-
values: VALUES.optional().describe(
|
|
143192
|
-
|
|
143840
|
+
values: VALUES.optional().describe(
|
|
143841
|
+
`Optional, exactly as for fill_form (ids may be shortened the same way): ${VALUE_SHAPES} Test values count as answers for the wording check of edit_template and save_template: prefer ones that are not words printed on the form (a caption that repeats one has to be confirmed there).`
|
|
143842
|
+
),
|
|
143843
|
+
dpi: external_exports.number().int().min(36).max(MAX_DPI).optional().describe(
|
|
143844
|
+
`Resolution (default ${String(DEFAULT_DPI)}, max ${String(MAX_DPI)}). The image's longest side is capped at ${String(PREVIEW_MAX_SIDE_PX)} px; the result's dpi is what was rendered (use crop + zoom for more detail).`
|
|
143845
|
+
),
|
|
143193
143846
|
crop: external_exports.object({
|
|
143194
143847
|
x: external_exports.number().min(0).max(1),
|
|
143195
143848
|
y: external_exports.number().min(0).max(1),
|
|
143196
143849
|
w: external_exports.number().gt(0).max(1),
|
|
143197
143850
|
h: external_exports.number().gt(0).max(1)
|
|
143198
143851
|
}).optional().describe(
|
|
143199
|
-
"Optional: render only this region of the page (fractions, top-left origin, like the legend boxes), magnified by zoom. The legend then lists only what the region shows."
|
|
143852
|
+
"Optional: render only this region of the page (fractions, top-left origin, like the legend boxes), magnified by zoom. The legend then lists only what the region shows. Convert a position read off the image by proportion (x = crop.x + crop.w \xD7 pixel x / image width, the same for y), not by pixel count: a client may show the image smaller than width \xD7 height."
|
|
143200
143853
|
),
|
|
143201
143854
|
zoom: external_exports.number().min(1).max(MAX_ZOOM).optional().describe(`With crop: magnification (default 2, max ${String(MAX_ZOOM)}).`),
|
|
143202
143855
|
legend: external_exports.enum(["all", "changed", "none"]).optional().describe(
|
|
143203
143856
|
`Which legend rows to return: "all" (default), "changed" (only what changed since your last preview of this page, or, the first time, since fenfill's version; plus removed ids), or "none".`
|
|
143857
|
+
),
|
|
143858
|
+
overlay: external_exports.enum(OVERLAYS).optional().describe(
|
|
143859
|
+
'What is drawn over the page: "full" (default) = every box outlined and tagged, table headers printed; "outline" = the boxes only, no id tags and no table header texts; "none" = nothing, the page as a fill stamps it. Use "outline" or "none" when a tag covers a caption, a tick box or a stamped value. The legend and placement_warnings are the same in all three.'
|
|
143204
143860
|
)
|
|
143205
143861
|
},
|
|
143206
143862
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
@@ -143230,16 +143886,17 @@ function createFenfillServer(opts = {}) {
|
|
|
143230
143886
|
render = wc ? wc.render : (await fetchTemplate(id, signal)).render;
|
|
143231
143887
|
bytes = await templateBlank(id, signal);
|
|
143232
143888
|
}
|
|
143233
|
-
|
|
143889
|
+
const answers = taken(render, values2);
|
|
143234
143890
|
const pv = await previewPage({
|
|
143235
143891
|
pdfBytes: bytes,
|
|
143236
143892
|
render,
|
|
143237
143893
|
page: args.page,
|
|
143238
|
-
values:
|
|
143894
|
+
values: answers,
|
|
143239
143895
|
dpi: args.dpi,
|
|
143240
143896
|
fonts: loadFonts(opts.fontsDir),
|
|
143241
143897
|
crop: args.crop,
|
|
143242
|
-
zoom: args.zoom
|
|
143898
|
+
zoom: args.zoom,
|
|
143899
|
+
overlay: args.overlay
|
|
143243
143900
|
});
|
|
143244
143901
|
const placement = await placementWarnings2(
|
|
143245
143902
|
bytes,
|
|
@@ -143272,7 +143929,13 @@ function createFenfillServer(opts = {}) {
|
|
|
143272
143929
|
edited: false,
|
|
143273
143930
|
source: args.path ? "analysis (no local edits)" : "saved template (no local edits)"
|
|
143274
143931
|
},
|
|
143275
|
-
|
|
143932
|
+
// The colour key of what is drawn: nothing with overlay "none".
|
|
143933
|
+
...args.overlay === "none" ? { overlay: "none" } : args.overlay === "outline" ? {
|
|
143934
|
+
overlay: "outline",
|
|
143935
|
+
colours: "green = filled, orange = filled with a warning, red = skipped, grey dashed = empty; a radio/multiselect option has a thin box"
|
|
143936
|
+
} : {
|
|
143937
|
+
colours: "green = filled, orange = filled with a warning, red = skipped, grey dashed = empty; a radio/multiselect option has a thin box tagged below it"
|
|
143938
|
+
},
|
|
143276
143939
|
...args.crop ? { crop: args.crop } : {},
|
|
143277
143940
|
...mode === "none" ? {} : changes ? {
|
|
143278
143941
|
legend: changes.changed,
|
|
@@ -143294,6 +143957,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143294
143957
|
]
|
|
143295
143958
|
};
|
|
143296
143959
|
} catch (e4) {
|
|
143960
|
+
untaken(args);
|
|
143297
143961
|
const err2 = asToolError(e4);
|
|
143298
143962
|
log("warn", "tool failed", { tool: "preview_page", code: err2.code, ms: Date.now() - t0 });
|
|
143299
143963
|
return fail(err2);
|
|
@@ -143308,10 +143972,15 @@ function createFenfillServer(opts = {}) {
|
|
|
143308
143972
|
inputSchema: {
|
|
143309
143973
|
path: TARGET_PATH,
|
|
143310
143974
|
template_id: TARGET_TEMPLATE,
|
|
143311
|
-
ops: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(MAX_OPS).describe(
|
|
143312
|
-
`Edit ops, applied in order (max ${String(MAX_OPS)}), each all-or-nothing; a box only has to lie on the page after the whole batch (so move then resize is fine). Fields: set_box {id, x, y, w, h, snap?} (absolute top-left + size; a group scales its members); move {id, dx, dy} or {id, x, y, snap?} (new top-left); resize {id, w, h}; add {page, type: text|multiline|date|checkbox|signature, box: {x, y, w, h}, label, description?, placeholder?, required?, section_id?, snap?} (diff_summary.added gives the new id); snap: "underline" fits the box onto the printed rule under it, "cell" into the ruled box around it, "answer" into that cell's blank part below (or after) its printed caption (read from the page on this machine; a miss keeps your box and warns); relabel {id, label}; retype {id, type} (a radio/multiselect group: radio|multiselect); delete {id}; set_format {id, format: {...}} (font_size, text_anchor, variant, date_format: DD/MM/YYYY|MM/DD/YYYY|YYYY-MM-DD|DD.MM.YYYY|DD-MM-YYYY; null removes a key; on a table cell it applies to the cell's whole column/row; a comb group: cell_type, date_format with one D/M/Y letter per cell); set_required {id, required}; set_description {id, description}; set_placeholder {id, placeholder} (null clears); set_autofill {id, autofill: a browser token (name, given-name, family-name, email, tel, street-address, postal-code, bday, \u2026) or null}. Groups: group {ids, kind: "choice"|"comb", label, multiple?} (standalone fields on one page; choice turns them into checkbox options); ungroup {id}; set_options {id, options: ["label", \u2026] in listed order, or [{id, label}]}; add_option {id: group, label, box, index?} and remove_option {id: group, option} keep the group's id. Tables: group {kind: "table", label, cells: [[id|null, \u2026], \u2026] (rows \xD7 columns) or ids (laid out from their boxes), header_cols?, header_rows?, orientation?: "col"|"row" (the axis whose lines carry the types; default col), format?: classic|expandable|checkbox_matrix}; table_insert {id, axis: "row"|"col", index, box?: {y, h} for a row / {x, w} for a column (default: one pitch past the edge, or centred in the gap), header?, type?}; table_delete {id, axis, index, keep_cells?}; table_add_cell {id, row, col, box?} (fills an empty slot); table_adopt {id, field, row, col} (moves a loose field into an empty slot); set_column_type {id, index, type: text|number|currency|date|checkbox, date_format?} (index along the typed axis); set_orientation {id, orientation} (resets every cell to text); set_header {id, axis, index, text}; set_headers {id, axis, texts: [...], start?} (several at once). Order: reorder {page, ids} moves those top-level entries together, in that order, to where the first sat (stamps order; geometry no longer decides). Every cell of one column (or row, when orientation is row) has one type.`
|
|
143975
|
+
ops: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(MAX_OPS).optional().describe(
|
|
143976
|
+
`Edit ops, applied in order (max ${String(MAX_OPS)}), each all-or-nothing; a box only has to lie on the page after the whole batch (so move then resize is fine). Fields: set_box {id, x, y, w, h, snap?} (absolute top-left + size; a group scales its members); move {id, dx, dy} or {id, x, y, snap?} (new top-left); resize {id, w, h}; add {page, type: text|multiline|date|checkbox|signature, box: {x, y, w, h}, label, description?, placeholder?, required?, section_id?, snap?} (diff_summary.added gives the new id); snap: "underline" fits the box onto the printed rule under it, "cell" into the ruled box around it, "answer" into that cell's blank part below (or after) its printed caption (read from the page on this machine; a miss keeps your box and warns); relabel {id, label}; retype {id, type} (a radio/multiselect group: radio|multiselect); delete {id}; set_format {id, format: {...}} (font_size, text_anchor, variant, date_format: DD/MM/YYYY|MM/DD/YYYY|YYYY-MM-DD|DD.MM.YYYY|DD-MM-YYYY; null removes a key; on a table cell it applies to the cell's whole column/row; a comb group: cell_type char|integer|date, or date_format with one D/M/Y letter per cell, e.g. DDMMYYYY, which makes it a date comb; diff_summary.ops shows the comb's cell_type, date_format and max_length); set_required {id, required}; set_description {id, description}; set_placeholder {id, placeholder} (null clears); set_autofill {id, autofill: a browser token (name, given-name, family-name, email, tel, street-address, postal-code, bday, \u2026) or null}. Groups: group {ids, kind: "choice"|"comb", label, multiple?} (standalone fields on one page; choice turns them into checkbox options); ungroup {id}; set_options {id, options: ["label", \u2026] in listed order, or [{id, label}]}; add_option {id: group, label, box, index?} and remove_option {id: group, option} keep the group's id. Tables: group {kind: "table", label, cells: [[id|null, \u2026], \u2026] (rows \xD7 columns) or ids (laid out from their boxes), header_cols?, header_rows?, orientation?: "col"|"row" (the axis whose lines carry the types; default col), format?: classic|expandable|checkbox_matrix}; table_insert {id, axis: "row"|"col", index, box?: {y, h} for a row / {x, w} for a column (default: one pitch past the edge, or centred in the gap), header?, type?}; table_delete {id, axis, index, keep_cells?}; table_add_cell {id, row, col, box?} (fills an empty slot); table_adopt {id, field, row, col} (moves a loose field into an empty slot); set_column_type {id, index, type: text|number|currency|date|checkbox, date_format?} (index along the typed axis); set_orientation {id, orientation} (resets every cell to text); set_header {id, axis, index, text}; set_headers {id, axis, texts: [...], start?} (several at once). Order: reorder {page, ids} moves those top-level entries together, in that order, to where the first sat (stamps order; geometry no longer decides); diff_summary.ops shows the order now listed. A section is listed as one run where its first entry stands, so entries of different sections never stand together: set_section {ids, section_id | null} moves fields or groups of one page to another of its sections (get_template shows each section's section_id; null = no section), and set_section_title {page, section_id, title} renames one. A reorder whose ids cannot come out as asked and that would change nothing in the listing is rejected. Every cell of one column (or row, when orientation is row) has one type.`
|
|
143313
143977
|
),
|
|
143314
|
-
discard: external_exports.boolean().optional().describe(
|
|
143978
|
+
discard: external_exports.boolean().optional().describe(
|
|
143979
|
+
"Drop the local working copy first (any ops then apply to fenfill's version). ops may be left out with it."
|
|
143980
|
+
),
|
|
143981
|
+
confirm_wording: external_exports.boolean().optional().describe(
|
|
143982
|
+
"Only after an op was rejected because its wording matches an answer of this session: true confirms the rejected wording is the form's own printed text, not an answer, so the same op applies. It covers only wording a rejection has already named (that text in that property); new wording is rejected and named first."
|
|
143983
|
+
)
|
|
143315
143984
|
},
|
|
143316
143985
|
annotations: {
|
|
143317
143986
|
readOnlyHint: false,
|
|
@@ -143320,8 +143989,15 @@ function createFenfillServer(opts = {}) {
|
|
|
143320
143989
|
openWorldHint: true
|
|
143321
143990
|
}
|
|
143322
143991
|
},
|
|
143323
|
-
run("edit_template", async (
|
|
143992
|
+
run("edit_template", async (args, _extra, signal) => {
|
|
143993
|
+
const { path, template_id, ops: given, discard, confirm_wording } = args;
|
|
143324
143994
|
oneTarget({ path, template_id });
|
|
143995
|
+
if (given === void 0 && discard !== true) {
|
|
143996
|
+
throw new ToolError("invalid_argument", "Pass ops, or discard: true (or both).", {
|
|
143997
|
+
hint: "ops: the edits to apply ([] only reports whether local edits exist); discard: true drops the local working copy."
|
|
143998
|
+
});
|
|
143999
|
+
}
|
|
144000
|
+
const ops = given ?? [];
|
|
143325
144001
|
const who = await identity2(false);
|
|
143326
144002
|
let key;
|
|
143327
144003
|
let saveTarget;
|
|
@@ -143352,6 +144028,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143352
144028
|
if (discard === true) discarded = await cache3.deleteEdits(key);
|
|
143353
144029
|
let wc = await cache3.readEdits(key);
|
|
143354
144030
|
if (wc && saveTarget && !sameAccount(wc, who)) wc = null;
|
|
144031
|
+
const had = wc !== null;
|
|
143355
144032
|
const nowIso = new Date(now()).toISOString();
|
|
143356
144033
|
if (!wc && ops.length === 0) {
|
|
143357
144034
|
return {
|
|
@@ -143393,7 +144070,10 @@ function createFenfillServer(opts = {}) {
|
|
|
143393
144070
|
snap = () => ({ none: `no page render (${why})` });
|
|
143394
144071
|
}
|
|
143395
144072
|
}
|
|
143396
|
-
const r = applyOps(wc.render, ops, (w) => echo.check(w), {
|
|
144073
|
+
const r = applyOps(wc.render, ops, (w) => echo.check(w, key), {
|
|
144074
|
+
snap,
|
|
144075
|
+
confirmWording: confirm_wording === true
|
|
144076
|
+
});
|
|
143397
144077
|
let placement = [];
|
|
143398
144078
|
if (r.applied > 0) {
|
|
143399
144079
|
const ids = [
|
|
@@ -143411,14 +144091,16 @@ function createFenfillServer(opts = {}) {
|
|
|
143411
144091
|
writer,
|
|
143412
144092
|
...inherited ? { inherited_wording: true } : {}
|
|
143413
144093
|
};
|
|
143414
|
-
const
|
|
143415
|
-
if (
|
|
144094
|
+
const changed = r.applied > 0 && JSON.stringify(r.render) !== JSON.stringify(wc.render);
|
|
144095
|
+
if (changed) {
|
|
143416
144096
|
await cache3.writeEdits(out);
|
|
143417
|
-
if (saveTarget && !
|
|
144097
|
+
if (saveTarget && !had) {
|
|
143418
144098
|
await dropFileCopies(saveTarget, fileSha).catch(() => void 0);
|
|
143419
144099
|
}
|
|
143420
144100
|
}
|
|
143421
|
-
|
|
144101
|
+
echo.tell(r.screened.told);
|
|
144102
|
+
echo.confirm(r.screened.confirmed);
|
|
144103
|
+
const hasEdits = changed || had;
|
|
143422
144104
|
const left = hasEdits ? schemaIssues(out.render, 10) : [];
|
|
143423
144105
|
return {
|
|
143424
144106
|
target: saveTarget ? { template_id: saveTarget } : { path },
|
|
@@ -143434,7 +144116,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143434
144116
|
)
|
|
143435
144117
|
],
|
|
143436
144118
|
has_edits: hasEdits,
|
|
143437
|
-
ops_applied_total: out.ops_applied,
|
|
144119
|
+
ops_applied_total: changed ? out.ops_applied : wc.ops_applied,
|
|
143438
144120
|
saveable: !!saveTarget,
|
|
143439
144121
|
next: saveTarget ? "Check with preview_page (or fill_form with check_pdf), then save_template to keep the edits in the saved template." : "These edits stay on this machine (fill_form and preview_page use them). This is an unsaved analysis, so save_template can't keep them: to keep corrections, analyze_form with save_as_template: true and edit that template."
|
|
143440
144122
|
};
|
|
@@ -143444,11 +144126,11 @@ function createFenfillServer(opts = {}) {
|
|
|
143444
144126
|
"save_template",
|
|
143445
144127
|
{
|
|
143446
144128
|
title: "Save local edits to a template",
|
|
143447
|
-
description: "Save edit_template's local working copy of a saved template to fenfill (only the layout and the form's wording are sent; never fill values). Fails safely if the template changed in fenfill since the edits began (template_conflict: the local edits are kept; discard and re-apply them), and refuses wording that repeats an answer given to fill_form / fill_template / preview_page in this session (answers never go into a template). Wording edited in an earlier session can't be checked that way: save_template then answers confirm_wording_needed with the changed ids and properties; review that wording (preview_page shows labels) and call again with confirm_wording: true. An unsaved analysis can't be saved: analyze it again with save_as_template: true (free on a native-field PDF; otherwise it costs scans again), then edit that template.",
|
|
144129
|
+
description: "Save edit_template's local working copy of a saved template to fenfill (only the layout and the form's wording are sent; never fill values). Fails safely if the template changed in fenfill since the edits began (template_conflict: the local edits are kept; discard and re-apply them), and refuses wording that repeats an answer given to fill_form / fill_template / preview_page in this session (answers never go into a template): answer_in_template lists the matched ids and properties. An answer: fix it with edit_template. The form's own wording (a caption that shares a word with a test value): call again with confirm_wording: true. Wording edited in an earlier session can't be checked that way: save_template then answers confirm_wording_needed with the changed ids and properties; review that wording (preview_page shows labels) and call again with confirm_wording: true. An unsaved analysis can't be saved: analyze it again with save_as_template: true (free on a native-field PDF; otherwise it costs scans again), then edit that template.",
|
|
143448
144130
|
inputSchema: {
|
|
143449
144131
|
template_id: TEMPLATE_ID,
|
|
143450
144132
|
confirm_wording: external_exports.boolean().optional().describe(
|
|
143451
|
-
"Only after
|
|
144133
|
+
"Only after an answer_in_template or confirm_wording_needed error: true confirms you compared the listed wording with the printed form and it is the form's own text, not an answer. An entry no error has listed yet is refused and listed first, on a copy from an earlier session too."
|
|
143452
144134
|
)
|
|
143453
144135
|
},
|
|
143454
144136
|
annotations: {
|
|
@@ -143469,19 +144151,29 @@ function createFenfillServer(opts = {}) {
|
|
|
143469
144151
|
};
|
|
143470
144152
|
}
|
|
143471
144153
|
const changed = changedWording(wc.base, wc.render);
|
|
143472
|
-
const echoed = echo.check(changed);
|
|
143473
|
-
|
|
144154
|
+
const echoed = echo.check(changed, wc.key);
|
|
144155
|
+
const foreign = wc.writer?.owner !== owner || wc.inherited_wording === true;
|
|
144156
|
+
const listing = sha256Hex(JSON.stringify(changed));
|
|
144157
|
+
const unvouched = foreign && changed.length > 0;
|
|
144158
|
+
const confirmable = confirm_wording === true && echoed.refused.every((x2) => x2.listed);
|
|
144159
|
+
if (echoed.refused.length && !confirmable) {
|
|
144160
|
+
echo.tell(echoed.refused);
|
|
144161
|
+
if (unvouched) listedForeign.set(wc.key, listing);
|
|
143474
144162
|
throw new ToolError(
|
|
143475
144163
|
"answer_in_template",
|
|
143476
|
-
"Some edited wording matches an answer you filled in this session, so it was not sent.",
|
|
144164
|
+
"Some edited wording matches an answer you filled or previewed in this session, so it was not sent.",
|
|
143477
144165
|
{
|
|
143478
|
-
hint: "
|
|
143479
|
-
details: {
|
|
144166
|
+
hint: "Compare each property listed in `matched` with the printed form (preview_page's legend shows the wording). An answer: put the form's own wording back with edit_template (relabel / set_description / set_placeholder / set_options / set_section_title). The form's own text: call save_template again with confirm_wording: true.",
|
|
144167
|
+
details: {
|
|
144168
|
+
ids: echoed.refuse,
|
|
144169
|
+
matched: wordingChanges(echoed.refused),
|
|
144170
|
+
...unvouched ? { changed: wordingChanges(changed) } : {}
|
|
144171
|
+
}
|
|
143480
144172
|
}
|
|
143481
144173
|
);
|
|
143482
144174
|
}
|
|
143483
|
-
|
|
143484
|
-
|
|
144175
|
+
if (unvouched && !(confirm_wording === true && listedForeign.get(wc.key) === listing)) {
|
|
144176
|
+
listedForeign.set(wc.key, listing);
|
|
143485
144177
|
throw new ToolError(
|
|
143486
144178
|
"confirm_wording_needed",
|
|
143487
144179
|
"This working copy has wording edited in an earlier session, which this session's answer check can't cover, so it was not sent.",
|
|
@@ -143511,7 +144203,13 @@ function createFenfillServer(opts = {}) {
|
|
|
143511
144203
|
}
|
|
143512
144204
|
);
|
|
143513
144205
|
}
|
|
143514
|
-
const schema = {
|
|
144206
|
+
const schema = {
|
|
144207
|
+
...wc.render,
|
|
144208
|
+
pages: wc.base.pages.map((p4, i2) => {
|
|
144209
|
+
const edited = wc.render.pages[i2];
|
|
144210
|
+
return edited?.page === p4.page && edited.sections !== void 0 ? { ...p4, sections: edited.sections } : p4;
|
|
144211
|
+
})
|
|
144212
|
+
};
|
|
143515
144213
|
let saved;
|
|
143516
144214
|
try {
|
|
143517
144215
|
saved = await api().patchJson(
|
|
@@ -143553,6 +144251,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143553
144251
|
throw e4;
|
|
143554
144252
|
}
|
|
143555
144253
|
await cache3.deleteEdits(wc.key);
|
|
144254
|
+
listedForeign.delete(wc.key);
|
|
143556
144255
|
await dropFileCopies(template_id, wc.sha256).catch(() => void 0);
|
|
143557
144256
|
await refreshSavedAnalyses(cache3, template_id, schema, saved, await identity2(false));
|
|
143558
144257
|
return {
|
|
@@ -143570,11 +144269,18 @@ function createFenfillServer(opts = {}) {
|
|
|
143570
144269
|
return {};
|
|
143571
144270
|
}
|
|
143572
144271
|
})(),
|
|
143573
|
-
...
|
|
143574
|
-
|
|
143575
|
-
|
|
143576
|
-
|
|
143577
|
-
|
|
144272
|
+
...(() => {
|
|
144273
|
+
const vouched = [...echoed.confirmed, ...echoed.refused];
|
|
144274
|
+
const warnings = [
|
|
144275
|
+
...vouched.length ? [
|
|
144276
|
+
`${describeWording(vouched)}: matches an answer filled in this session; saved because you confirmed it is the form's own wording`
|
|
144277
|
+
] : [],
|
|
144278
|
+
...echoed.warn.length ? [
|
|
144279
|
+
`${echoed.warn.join(", ")}: short wording that matches a short answer from this session (allowed under 4 characters); check it is the form's own text`
|
|
144280
|
+
] : []
|
|
144281
|
+
];
|
|
144282
|
+
return warnings.length ? { warnings } : {};
|
|
144283
|
+
})(),
|
|
143578
144284
|
next: "fill_template, get_template and recipient links now use the saved layout."
|
|
143579
144285
|
};
|
|
143580
144286
|
})
|