@fenfill/mcp 0.2.1 → 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 +84 -35
- package/THIRD_PARTY_NOTICES +1 -1
- package/dist/index.js +744 -158
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -136506,6 +136506,14 @@ function mapInternal(schema, input2, loadImage2) {
|
|
|
136506
136506
|
}
|
|
136507
136507
|
return out;
|
|
136508
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
|
+
}
|
|
136509
136517
|
async function verifyImages(m) {
|
|
136510
136518
|
if (m.images.length === 0) return;
|
|
136511
136519
|
const { PDFDocument: PDFDocument2 } = await Promise.resolve().then(() => __toESM(require_cjs()));
|
|
@@ -136627,15 +136635,15 @@ var CheckCopyError = class extends Error {
|
|
|
136627
136635
|
this.at = { ...at };
|
|
136628
136636
|
}
|
|
136629
136637
|
};
|
|
136630
|
-
async function buildCheckPdf(stamped, entries, status, fonts, banner = true) {
|
|
136638
|
+
async function buildCheckPdf(stamped, entries, status, fonts, banner = true, tags = true) {
|
|
136631
136639
|
const at = { stage: "load" };
|
|
136632
136640
|
try {
|
|
136633
|
-
return await drawCheckPdf(stamped, entries, status, fonts, banner, at);
|
|
136641
|
+
return await drawCheckPdf(stamped, entries, status, fonts, banner, tags, at);
|
|
136634
136642
|
} catch (e4) {
|
|
136635
136643
|
throw new CheckCopyError(at, e4);
|
|
136636
136644
|
}
|
|
136637
136645
|
}
|
|
136638
|
-
async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
|
|
136646
|
+
async function drawCheckPdf(stamped, entries, status, fonts, banner, tags, at) {
|
|
136639
136647
|
const step = (stage, id) => {
|
|
136640
136648
|
at.stage = stage;
|
|
136641
136649
|
at.id = id;
|
|
@@ -136684,7 +136692,7 @@ async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
|
|
|
136684
136692
|
const headColor = rgb3(0.15, 0.35, 0.75);
|
|
136685
136693
|
const wanted = [];
|
|
136686
136694
|
for (const e4 of entries) {
|
|
136687
|
-
if (e4.kind !== "table") continue;
|
|
136695
|
+
if (!tags || e4.kind !== "table") continue;
|
|
136688
136696
|
step("table headers", e4.id);
|
|
136689
136697
|
const g = e4.group;
|
|
136690
136698
|
const texts = (v) => Array.isArray(v) ? v : [];
|
|
@@ -136783,7 +136791,7 @@ async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
|
|
|
136783
136791
|
opacity: 0,
|
|
136784
136792
|
borderOpacity: 0.9
|
|
136785
136793
|
});
|
|
136786
|
-
if (!b.tagged) continue;
|
|
136794
|
+
if (!tags || !b.tagged) continue;
|
|
136787
136795
|
step("tag", b.id);
|
|
136788
136796
|
const tag = asciiOnly(short.get(b.id) ?? b.id);
|
|
136789
136797
|
const tw = helv.widthOfTextAtSize(tag, SIZE) + 1.6;
|
|
@@ -136874,7 +136882,8 @@ async function fillAgentPdf(a) {
|
|
|
136874
136882
|
m.entries,
|
|
136875
136883
|
(id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
|
|
136876
136884
|
a.fonts,
|
|
136877
|
-
a.checkBanner !== false
|
|
136885
|
+
a.checkBanner !== false,
|
|
136886
|
+
a.checkTags !== false
|
|
136878
136887
|
);
|
|
136879
136888
|
res.checkPdf = built.pdf;
|
|
136880
136889
|
warnings.push(...built.notes);
|
|
@@ -137478,19 +137487,24 @@ function listedPages(agent, requested) {
|
|
|
137478
137487
|
return have.filter((p4) => want.has(p4));
|
|
137479
137488
|
}
|
|
137480
137489
|
function nestBySection(entries) {
|
|
137481
|
-
const blocks =
|
|
137490
|
+
const blocks = [];
|
|
137491
|
+
let run = null;
|
|
137482
137492
|
for (const e4 of entries) {
|
|
137483
|
-
const sid = e4.section?.id
|
|
137484
|
-
const key = `${String(e4.page)}\0${sid}`;
|
|
137485
|
-
|
|
137486
|
-
if (!b) {
|
|
137493
|
+
const sid = e4.section?.id;
|
|
137494
|
+
const key = `${String(e4.page)}\0${sid ?? ""}`;
|
|
137495
|
+
if (key !== run) {
|
|
137487
137496
|
const title = e4.section?.title;
|
|
137488
|
-
|
|
137489
|
-
|
|
137497
|
+
blocks.push({
|
|
137498
|
+
page: e4.page,
|
|
137499
|
+
...sid ? { section_id: sid } : {},
|
|
137500
|
+
...title ? { title } : {},
|
|
137501
|
+
fields: []
|
|
137502
|
+
});
|
|
137503
|
+
run = key;
|
|
137490
137504
|
}
|
|
137491
|
-
|
|
137505
|
+
blocks[blocks.length - 1].fields.push(compactEntry(e4));
|
|
137492
137506
|
}
|
|
137493
|
-
return
|
|
137507
|
+
return blocks;
|
|
137494
137508
|
}
|
|
137495
137509
|
function compactView(agent, requested, next, maxChars = COMPACT_MAX_CHARS) {
|
|
137496
137510
|
const entries = selectEntries(agent, requested);
|
|
@@ -137784,7 +137798,7 @@ function loadFonts(dir = defaultFontsDir()) {
|
|
|
137784
137798
|
}
|
|
137785
137799
|
|
|
137786
137800
|
// src/version.ts
|
|
137787
|
-
var VERSION = true ? "0.2.
|
|
137801
|
+
var VERSION = true ? "0.2.2" : "0.0.0-dev";
|
|
137788
137802
|
var USER_AGENT = `fenfill-mcp/${VERSION}`;
|
|
137789
137803
|
|
|
137790
137804
|
// src/fill.ts
|
|
@@ -139307,67 +139321,208 @@ var ECHO_CONTAINS_MIN = 6;
|
|
|
139307
139321
|
var MAX_REMEMBERED = 5e3;
|
|
139308
139322
|
var MAX_LEN = 2e3;
|
|
139309
139323
|
var FREE_TEXT = /* @__PURE__ */ new Set(["text", "multiline", "date", "comb"]);
|
|
139310
|
-
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
|
+
}
|
|
139311
139362
|
var EchoGuard = class {
|
|
139312
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();
|
|
139313
139371
|
add(v) {
|
|
139314
139372
|
if (typeof v === "number" && Number.isFinite(v)) v = String(v);
|
|
139315
139373
|
if (typeof v !== "string") return;
|
|
139316
139374
|
const n = norm(v);
|
|
139317
139375
|
if (!n || n.length > MAX_LEN) return;
|
|
139318
|
-
this.seen
|
|
139319
|
-
|
|
139320
|
-
|
|
139321
|
-
const
|
|
139322
|
-
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);
|
|
139323
139392
|
}
|
|
139324
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));
|
|
139432
|
+
}
|
|
139433
|
+
for (const [, text] of shown) this.add(text);
|
|
139434
|
+
}
|
|
139325
139435
|
/** Remember the free-text values of one fill/preview call. */
|
|
139326
139436
|
remember(render, values2) {
|
|
139437
|
+
let schema;
|
|
139327
139438
|
let entries;
|
|
139328
139439
|
try {
|
|
139329
|
-
|
|
139440
|
+
schema = normalizeSchema(render);
|
|
139441
|
+
entries = agentEntries(schema);
|
|
139330
139442
|
} catch {
|
|
139443
|
+
this.addAny(values2);
|
|
139331
139444
|
return;
|
|
139332
139445
|
}
|
|
139333
|
-
|
|
139446
|
+
this.addShown(schema, entries, values2);
|
|
139334
139447
|
for (const e4 of entries) {
|
|
139335
139448
|
const v = values2[e4.id];
|
|
139336
139449
|
if (v === void 0) continue;
|
|
139337
139450
|
if (e4.kind === "field" || e4.kind === "comb") {
|
|
139338
139451
|
if (FREE_TEXT.has(e4.type)) this.add(v);
|
|
139339
|
-
} else if (e4.kind === "table" &&
|
|
139452
|
+
} else if (e4.kind === "table" && isRec(v)) {
|
|
139340
139453
|
for (const c2 of e4.cells) if (FREE_TEXT.has(c2.type)) this.add(v[c2.id]);
|
|
139341
139454
|
} else if (e4.kind === "table_rows" && Array.isArray(v)) {
|
|
139455
|
+
const ids = new Set(e4.columns.map((c2) => c2.id));
|
|
139342
139456
|
for (const row of v) {
|
|
139343
|
-
if (!
|
|
139457
|
+
if (!isRec(row)) continue;
|
|
139344
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);
|
|
139345
139460
|
}
|
|
139346
139461
|
}
|
|
139347
139462
|
}
|
|
139348
139463
|
}
|
|
139349
|
-
/** Wording that repeats a remembered answer: refusals (it
|
|
139350
|
-
*
|
|
139351
|
-
|
|
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 = "") {
|
|
139352
139470
|
const refuse = /* @__PURE__ */ new Set();
|
|
139353
139471
|
const warn = /* @__PURE__ */ new Set();
|
|
139354
|
-
|
|
139472
|
+
const refused = [];
|
|
139473
|
+
const confirmed = [];
|
|
139474
|
+
if (this.seen.size === 0) return { refuse: [], warn: [], refused, confirmed };
|
|
139355
139475
|
for (const w of wording) {
|
|
139356
139476
|
const n = norm(w.text);
|
|
139357
139477
|
if (!n) continue;
|
|
139478
|
+
let hit = false;
|
|
139358
139479
|
if (this.seen.has(n)) {
|
|
139359
|
-
(n.length
|
|
139360
|
-
|
|
139361
|
-
|
|
139362
|
-
if (n.length < ECHO_CONTAINS_MIN) continue;
|
|
139363
|
-
for (const v of this.seen) {
|
|
139364
|
-
if (v.length >= ECHO_CONTAINS_MIN && n.includes(v)) {
|
|
139365
|
-
refuse.add(w.id);
|
|
139366
|
-
break;
|
|
139480
|
+
if (n.length < ECHO_REFUSE_MIN) {
|
|
139481
|
+
warn.add(w.id);
|
|
139482
|
+
continue;
|
|
139367
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)) });
|
|
139368
139506
|
}
|
|
139369
139507
|
}
|
|
139370
|
-
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));
|
|
139371
139526
|
}
|
|
139372
139527
|
get size() {
|
|
139373
139528
|
return this.seen.size;
|
|
@@ -139377,13 +139532,121 @@ var EchoGuard = class {
|
|
|
139377
139532
|
// src/edits.ts
|
|
139378
139533
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
139379
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
|
+
|
|
139380
139643
|
// src/doc.ts
|
|
139381
139644
|
var OpError = class extends Error {
|
|
139382
139645
|
};
|
|
139383
139646
|
var bad = (msg) => {
|
|
139384
139647
|
throw new OpError(msg);
|
|
139385
139648
|
};
|
|
139386
|
-
var
|
|
139649
|
+
var isRec2 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
|
|
139387
139650
|
var isNum = (x2) => typeof x2 === "number" && Number.isFinite(x2);
|
|
139388
139651
|
var round = (n, d = 4) => Math.round(n * 10 ** d) / 10 ** d;
|
|
139389
139652
|
function index(s) {
|
|
@@ -139497,7 +139760,7 @@ function checkBoxOnPage(b) {
|
|
|
139497
139760
|
if (!onPage(b)) bad(ON_PAGE);
|
|
139498
139761
|
}
|
|
139499
139762
|
function boxArg(v, name = "box") {
|
|
139500
|
-
if (!
|
|
139763
|
+
if (!isRec2(v)) return bad(`${name} {x, y, w, h} is required (fractions of the page, top-left)`);
|
|
139501
139764
|
const b = {
|
|
139502
139765
|
x: checkFraction(v.x, `${name}.x`),
|
|
139503
139766
|
y: checkFraction(v.y, `${name}.y`),
|
|
@@ -139575,7 +139838,7 @@ function patchForDataType(t, dateFormat) {
|
|
|
139575
139838
|
}
|
|
139576
139839
|
var DEFAULT_TEXT = { type: "text", format: { variant: "any" } };
|
|
139577
139840
|
function variantOf(f3) {
|
|
139578
|
-
return
|
|
139841
|
+
return isRec2(f3.format) && typeof f3.format.variant === "string" ? f3.format.variant : "any";
|
|
139579
139842
|
}
|
|
139580
139843
|
function isDefaultText2(f3) {
|
|
139581
139844
|
return f3.type === "text" && variantOf(f3) === "any";
|
|
@@ -139687,7 +139950,7 @@ function lineType(d, v, i2) {
|
|
|
139687
139950
|
const w = cells.find((c2) => !isDefaultText2(c2)) ?? cells[0];
|
|
139688
139951
|
return {
|
|
139689
139952
|
type: String(w.type),
|
|
139690
|
-
...
|
|
139953
|
+
...isRec2(w.format) ? { format: structuredClone(w.format) } : {}
|
|
139691
139954
|
};
|
|
139692
139955
|
}
|
|
139693
139956
|
var axisWord = (a) => a === "col" ? "column" : "row";
|
|
@@ -139962,7 +140225,7 @@ function tableInsert(d, op, w) {
|
|
|
139962
140225
|
}
|
|
139963
140226
|
let at;
|
|
139964
140227
|
if (op.box !== void 0) {
|
|
139965
|
-
if (!
|
|
140228
|
+
if (!isRec2(op.box)) return bad("box must be an object");
|
|
139966
140229
|
const b = op.box;
|
|
139967
140230
|
const [p4, s] = axis === "row" ? ["y", "h"] : ["x", "w"];
|
|
139968
140231
|
const pos = b[p4];
|
|
@@ -140033,8 +140296,8 @@ function tableDelete(d, op, w) {
|
|
|
140033
140296
|
}
|
|
140034
140297
|
if (v.cols === 0) v.grid = [];
|
|
140035
140298
|
v.rows = v.grid.length;
|
|
140036
|
-
const
|
|
140037
|
-
if (
|
|
140299
|
+
const keep2 = op.keep_cells === true;
|
|
140300
|
+
if (keep2) {
|
|
140038
140301
|
for (const id of ids) {
|
|
140039
140302
|
const f3 = d.field.get(id);
|
|
140040
140303
|
if (f3) f3.group = null;
|
|
@@ -140043,9 +140306,9 @@ function tableDelete(d, op, w) {
|
|
|
140043
140306
|
} else removeFields(d, ids);
|
|
140044
140307
|
const alive = commit(d, v, hc, hr);
|
|
140045
140308
|
if (!alive) w.push(`that was the last line of table ${v.g.id}, so the table was removed`);
|
|
140046
|
-
const deleted = [...
|
|
140309
|
+
const deleted = [...keep2 ? [] : ids, ...alive ? [] : [v.g.id]];
|
|
140047
140310
|
return {
|
|
140048
|
-
touched: [...alive ? [v.g.id] : [], ...
|
|
140311
|
+
touched: [...alive ? [v.g.id] : [], ...keep2 ? ids : []],
|
|
140049
140312
|
deleted,
|
|
140050
140313
|
tables: alive ? [v.g.id] : []
|
|
140051
140314
|
};
|
|
@@ -140096,7 +140359,7 @@ function tableAdopt(d, op, w) {
|
|
|
140096
140359
|
const { r, c: c2 } = slotArgs(v, op);
|
|
140097
140360
|
const tf = slotType(d, v, r, c2, {
|
|
140098
140361
|
type: String(f3.type),
|
|
140099
|
-
...
|
|
140362
|
+
...isRec2(f3.format) ? { format: f3.format } : {}
|
|
140100
140363
|
});
|
|
140101
140364
|
if (!sameType(f3, tf)) {
|
|
140102
140365
|
w.push(
|
|
@@ -140335,7 +140598,7 @@ var FORMAT_KEYS = {
|
|
|
140335
140598
|
date: /* @__PURE__ */ new Set(["date_format", "font_size", "text_anchor"]),
|
|
140336
140599
|
signature: /* @__PURE__ */ new Set(["scale", "text_anchor"])
|
|
140337
140600
|
};
|
|
140338
|
-
var
|
|
140601
|
+
var isRec3 = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2);
|
|
140339
140602
|
var isNum2 = (x2) => typeof x2 === "number" && Number.isFinite(x2);
|
|
140340
140603
|
var isInt = (x2) => isNum2(x2) && Number.isInteger(x2);
|
|
140341
140604
|
function checkStr(out, v, max2, at, optional2) {
|
|
@@ -140368,7 +140631,7 @@ function checkAnchor(out, v, at) {
|
|
|
140368
140631
|
function formatIssues(type, fmt3, id, path) {
|
|
140369
140632
|
const out = [];
|
|
140370
140633
|
if (fmt3 === void 0 || fmt3 === null) return out;
|
|
140371
|
-
if (!
|
|
140634
|
+
if (!isRec3(fmt3)) return [{ id, path, reason: "format must be an object" }];
|
|
140372
140635
|
const allowed = typeof type === "string" ? FORMAT_KEYS[type] : void 0;
|
|
140373
140636
|
if (!allowed) return out;
|
|
140374
140637
|
for (const k4 of Object.keys(fmt3)) {
|
|
@@ -140405,7 +140668,7 @@ function formatIssues(type, fmt3, id, path) {
|
|
|
140405
140668
|
return out;
|
|
140406
140669
|
}
|
|
140407
140670
|
function fieldIssues(f3, path) {
|
|
140408
|
-
if (!
|
|
140671
|
+
if (!isRec3(f3)) return [{ path, reason: "must be an object" }];
|
|
140409
140672
|
const id = typeof f3.id === "string" ? f3.id : void 0;
|
|
140410
140673
|
const out = [];
|
|
140411
140674
|
const at = (k4) => ({ id, path: `${path}.${k4}` });
|
|
@@ -140428,7 +140691,7 @@ function fieldIssues(f3, path) {
|
|
|
140428
140691
|
else {
|
|
140429
140692
|
f3.cells.forEach((c2, i2) => {
|
|
140430
140693
|
const cp = { id, path: `${path}.cells.${String(i2)}` };
|
|
140431
|
-
if (!
|
|
140694
|
+
if (!isRec3(c2)) {
|
|
140432
140695
|
out.push({ ...cp, reason: "must be an object" });
|
|
140433
140696
|
return;
|
|
140434
140697
|
}
|
|
@@ -140454,7 +140717,7 @@ function fieldIssues(f3, path) {
|
|
|
140454
140717
|
return out;
|
|
140455
140718
|
}
|
|
140456
140719
|
function groupIssues(g, path) {
|
|
140457
|
-
if (!
|
|
140720
|
+
if (!isRec3(g)) return [{ path, reason: "must be an object" }];
|
|
140458
140721
|
const id = typeof g.id === "string" ? g.id : void 0;
|
|
140459
140722
|
const out = [];
|
|
140460
140723
|
const at = (k4) => ({ id, path: `${path}.${k4}` });
|
|
@@ -140512,7 +140775,7 @@ function groupIssues(g, path) {
|
|
|
140512
140775
|
return out;
|
|
140513
140776
|
}
|
|
140514
140777
|
function sectionIssues(s, path) {
|
|
140515
|
-
if (!
|
|
140778
|
+
if (!isRec3(s)) return [{ path, reason: "must be an object" }];
|
|
140516
140779
|
const id = typeof s.id === "string" ? s.id : void 0;
|
|
140517
140780
|
const out = [];
|
|
140518
140781
|
const at = (k4) => ({ id, path: `${path}.${k4}` });
|
|
@@ -140537,7 +140800,7 @@ function schemaIssues(schema, limit = 50) {
|
|
|
140537
140800
|
if (!Array.isArray(s.pages)) out.push({ path: "pages", reason: "must be a list" });
|
|
140538
140801
|
pages.forEach((p4, i2) => {
|
|
140539
140802
|
const path = `pages.${String(i2)}`;
|
|
140540
|
-
if (!
|
|
140803
|
+
if (!isRec3(p4)) {
|
|
140541
140804
|
out.push({ path, reason: "must be an object" });
|
|
140542
140805
|
return;
|
|
140543
140806
|
}
|
|
@@ -140638,7 +140901,9 @@ var OP_NAMES = [
|
|
|
140638
140901
|
"set_orientation",
|
|
140639
140902
|
"set_header",
|
|
140640
140903
|
"set_headers",
|
|
140641
|
-
"reorder"
|
|
140904
|
+
"reorder",
|
|
140905
|
+
"set_section",
|
|
140906
|
+
"set_section_title"
|
|
140642
140907
|
];
|
|
140643
140908
|
function placeBox(fs, bb, nb) {
|
|
140644
140909
|
if (nb.w <= 0 || nb.h <= 0) bad("the box must have a positive width and height");
|
|
@@ -140664,25 +140929,25 @@ function sectionIds(d, page) {
|
|
|
140664
140929
|
return out;
|
|
140665
140930
|
}
|
|
140666
140931
|
function typeAndFormat(t, prev) {
|
|
140667
|
-
const prevFmt =
|
|
140668
|
-
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]]));
|
|
140669
140934
|
switch (t) {
|
|
140670
140935
|
case "text":
|
|
140671
140936
|
return {
|
|
140672
140937
|
type: "text",
|
|
140673
|
-
format: { ...
|
|
140938
|
+
format: { ...keep2(["font_size", "text_anchor", "uppercase"]), variant: "any" }
|
|
140674
140939
|
};
|
|
140675
140940
|
case "multiline":
|
|
140676
140941
|
return {
|
|
140677
140942
|
type: "text",
|
|
140678
|
-
format: { ...
|
|
140943
|
+
format: { ...keep2(["font_size", "uppercase"]), variant: "multiline" }
|
|
140679
140944
|
};
|
|
140680
140945
|
case "date":
|
|
140681
140946
|
return {
|
|
140682
140947
|
type: "date",
|
|
140683
140948
|
format: {
|
|
140684
140949
|
date_format: typeof prevFmt.date_format === "string" ? prevFmt.date_format : "DD/MM/YYYY",
|
|
140685
|
-
...
|
|
140950
|
+
...keep2(["font_size", "text_anchor"])
|
|
140686
140951
|
}
|
|
140687
140952
|
};
|
|
140688
140953
|
case "checkbox":
|
|
@@ -140710,7 +140975,7 @@ function entryType(d, id) {
|
|
|
140710
140975
|
}
|
|
140711
140976
|
const f3 = d.field.get(id);
|
|
140712
140977
|
if (!f3) return "?";
|
|
140713
|
-
if (f3.type === "text" &&
|
|
140978
|
+
if (f3.type === "text" && isRec2(f3.format) && f3.format.variant === "multiline") return "multiline";
|
|
140714
140979
|
if ((f3.type === "text" || f3.type === "date") && Array.isArray(f3.cells) && f3.cells.length > 1)
|
|
140715
140980
|
return "comb";
|
|
140716
140981
|
return String(f3.type);
|
|
@@ -140758,7 +141023,7 @@ function checkSnapTarget(g, op) {
|
|
|
140758
141023
|
if (op.snap !== void 0 && g) bad("snap applies to a single field, not a group");
|
|
140759
141024
|
}
|
|
140760
141025
|
function applyOne(d, op, warnings, opts) {
|
|
140761
|
-
if (!
|
|
141026
|
+
if (!isRec2(op) || typeof op.op !== "string") return bad("each op needs an `op` name");
|
|
140762
141027
|
const raw = op;
|
|
140763
141028
|
const table = (r) => r;
|
|
140764
141029
|
switch (op.op) {
|
|
@@ -140916,29 +141181,36 @@ function applyOne(d, op, warnings, opts) {
|
|
|
140916
141181
|
}
|
|
140917
141182
|
case "set_format": {
|
|
140918
141183
|
const { f: f3, g } = needId(d, op.id);
|
|
140919
|
-
if (!
|
|
141184
|
+
if (!isRec2(op.format)) return bad("format must be an object");
|
|
140920
141185
|
if (g) {
|
|
140921
141186
|
if (g.kind !== "comb") {
|
|
140922
141187
|
return bad(
|
|
140923
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)"
|
|
140924
141189
|
);
|
|
140925
141190
|
}
|
|
141191
|
+
const comb = g;
|
|
141192
|
+
const dated = op.format.date_format !== void 0 && op.format.date_format !== null;
|
|
140926
141193
|
for (const [k4, v] of Object.entries(op.format)) {
|
|
140927
141194
|
if (k4 !== "cell_type" && k4 !== "date_format")
|
|
140928
141195
|
bad(`a comb group's format keys are cell_type and date_format, not ${k4}`);
|
|
140929
141196
|
if (k4 === "date_format" && v !== null) checkCombDateFormat(d, g, v);
|
|
140930
|
-
if (v === null) delete
|
|
140931
|
-
else
|
|
141197
|
+
if (v === null) delete comb[k4];
|
|
141198
|
+
else comb[k4] = v;
|
|
140932
141199
|
}
|
|
140933
|
-
|
|
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 };
|
|
140934
141206
|
}
|
|
140935
141207
|
const fld = f3;
|
|
140936
141208
|
const own2 = d.owner.get(fld.id);
|
|
140937
141209
|
if (own2 && (own2.kind === "comb" || own2.kind === "choice")) {
|
|
140938
141210
|
bad(`this is a member of ${String(own2.kind)} group ${own2.id}, whose kind fixes its format`);
|
|
140939
141211
|
}
|
|
140940
|
-
if (fld.type === "signature" && !
|
|
140941
|
-
const next = { ...
|
|
141212
|
+
if (fld.type === "signature" && !isRec2(fld.format)) fld.format = {};
|
|
141213
|
+
const next = { ...isRec2(fld.format) ? fld.format : {} };
|
|
140942
141214
|
for (const [k4, v] of Object.entries(op.format)) {
|
|
140943
141215
|
if (k4 === "date_format" && v !== null && !DATE_FORMATS.includes(v))
|
|
140944
141216
|
bad(`date_format must be one of ${DATE_FORMATS.join(", ")}`);
|
|
@@ -140953,7 +141225,7 @@ function applyOne(d, op, warnings, opts) {
|
|
|
140953
141225
|
delete fld.format;
|
|
140954
141226
|
else fld.format = next;
|
|
140955
141227
|
if (own2?.kind === "table") {
|
|
140956
|
-
applyFormatToLine(d, own2, fld,
|
|
141228
|
+
applyFormatToLine(d, own2, fld, isRec2(fld.format) ? fld.format : void 0, warnings);
|
|
140957
141229
|
return { touched: [fld.id, own2.id], tables: [own2.id] };
|
|
140958
141230
|
}
|
|
140959
141231
|
return { touched: [fld.id] };
|
|
@@ -141040,7 +141312,7 @@ function applyOne(d, op, warnings, opts) {
|
|
|
141040
141312
|
});
|
|
141041
141313
|
} else {
|
|
141042
141314
|
for (const o of op.options) {
|
|
141043
|
-
if (!
|
|
141315
|
+
if (!isRec2(o) || typeof o.id !== "string" || typeof o.label !== "string") {
|
|
141044
141316
|
return bad("options must be all strings, or all {id, label} pairs");
|
|
141045
141317
|
}
|
|
141046
141318
|
const oid = resolveId(d, o.id, ["field"], "option id");
|
|
@@ -141061,7 +141333,7 @@ function applyOne(d, op, warnings, opts) {
|
|
|
141061
141333
|
const n = ms.length;
|
|
141062
141334
|
if (op.index !== void 0 && (!Number.isInteger(op.index) || op.index < 0 || op.index > n))
|
|
141063
141335
|
return bad(`index must be 0\u2013${String(n)} (${String(n)} = last)`);
|
|
141064
|
-
const sib = ms.find((m) => m.type === "checkbox" &&
|
|
141336
|
+
const sib = ms.find((m) => m.type === "checkbox" && isRec2(m.format));
|
|
141065
141337
|
const f3 = {
|
|
141066
141338
|
id: randomUUID2(),
|
|
141067
141339
|
label: op.label.trim(),
|
|
@@ -141141,29 +141413,31 @@ function applyOne(d, op, warnings, opts) {
|
|
|
141141
141413
|
case "set_headers":
|
|
141142
141414
|
return table(setHeaders(d, raw, warnings));
|
|
141143
141415
|
case "reorder":
|
|
141144
|
-
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);
|
|
141145
141421
|
default:
|
|
141146
141422
|
return bad(`unknown op "${String(op.op)}"; ops: ${OP_NAMES.join(", ")}`);
|
|
141147
141423
|
}
|
|
141148
141424
|
}
|
|
141149
|
-
function reorder(d, op) {
|
|
141425
|
+
function reorder(d, op, warnings) {
|
|
141150
141426
|
const page = pageOf(d, op.page);
|
|
141151
141427
|
if (!Array.isArray(op.ids) || op.ids.length === 0)
|
|
141152
141428
|
return bad("ids must list the entries to order");
|
|
141153
|
-
const
|
|
141429
|
+
const orderOf2 = (item) => typeof item.order === "number" ? item.order : void 0;
|
|
141430
|
+
const unordered = [];
|
|
141154
141431
|
for (const f3 of d.s.fields) {
|
|
141155
|
-
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) });
|
|
141156
141434
|
}
|
|
141157
141435
|
for (const g of d.s.groups) {
|
|
141158
141436
|
if (g.page !== page) continue;
|
|
141159
|
-
const
|
|
141160
|
-
if (
|
|
141437
|
+
const edges = membersEdges(members2(d, g));
|
|
141438
|
+
if (edges) unordered.push({ ...edges, item: g, order: orderOf2(g) });
|
|
141161
141439
|
}
|
|
141162
|
-
const
|
|
141163
|
-
const hasOrder = blocks.some((b) => orderOf(b) !== void 0);
|
|
141164
|
-
blocks.sort(
|
|
141165
|
-
(a, b) => (hasOrder ? (orderOf(a) ?? Infinity) - (orderOf(b) ?? Infinity) || 0 : 0) || a.top - b.top || a.left - b.left
|
|
141166
|
-
);
|
|
141440
|
+
const blocks = orderBlocks(unordered);
|
|
141167
141441
|
const byId = new Map(blocks.map((b) => [b.item.id, b]));
|
|
141168
141442
|
const want = op.ids.map((raw) => {
|
|
141169
141443
|
const id = resolveId(d, raw, ["field", "group"], "ids entry");
|
|
@@ -141184,10 +141458,132 @@ function reorder(d, op) {
|
|
|
141184
141458
|
...want.map((id) => byId.get(id)),
|
|
141185
141459
|
...rest.slice(before)
|
|
141186
141460
|
];
|
|
141461
|
+
const was = listedOrder(d, page);
|
|
141187
141462
|
next.forEach((b, i2) => {
|
|
141188
141463
|
b.item.order = i2;
|
|
141189
141464
|
});
|
|
141190
|
-
|
|
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 } };
|
|
141191
141587
|
}
|
|
141192
141588
|
function checkCombDateFormat(d, g, v) {
|
|
141193
141589
|
const slots = members2(d, g).reduce(
|
|
@@ -141200,6 +141596,18 @@ function checkCombDateFormat(d, g, v) {
|
|
|
141200
141596
|
);
|
|
141201
141597
|
}
|
|
141202
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
|
+
}
|
|
141203
141611
|
function touchedIssues(d, ids, tables = []) {
|
|
141204
141612
|
const out = [];
|
|
141205
141613
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -141241,6 +141649,8 @@ function applyPass(render, ops, excluded, screen, opts) {
|
|
|
141241
141649
|
const touchedIds = /* @__PURE__ */ new Set();
|
|
141242
141650
|
const warnings = [];
|
|
141243
141651
|
const outcomes = [];
|
|
141652
|
+
const told = [];
|
|
141653
|
+
const confirmed = [];
|
|
141244
141654
|
let applied = 0;
|
|
141245
141655
|
ops.forEach((raw, i2) => {
|
|
141246
141656
|
const skip = excluded.get(i2);
|
|
@@ -141259,30 +141669,46 @@ function applyPass(render, ops, excluded, screen, opts) {
|
|
|
141259
141669
|
issues.slice(0, 5).map((x2) => `${x2.path} ${x2.reason}`).join("; ")
|
|
141260
141670
|
);
|
|
141261
141671
|
}
|
|
141672
|
+
let vouched = [];
|
|
141262
141673
|
if (screen) {
|
|
141263
141674
|
const changed = changedWording(cur, draft);
|
|
141264
|
-
const echo = changed.length ? screen(changed) :
|
|
141265
|
-
if (echo.
|
|
141266
|
-
|
|
141267
|
-
|
|
141268
|
-
)
|
|
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
|
+
}
|
|
141269
141690
|
}
|
|
141270
|
-
for (const id of echo
|
|
141691
|
+
for (const id of echo?.warn ?? []) {
|
|
141271
141692
|
w.push(
|
|
141272
|
-
`${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`
|
|
141273
141694
|
);
|
|
141274
141695
|
}
|
|
141275
141696
|
}
|
|
141276
141697
|
const name = raw.op;
|
|
141277
141698
|
const noop = JSON.stringify(draft) === JSON.stringify(cur);
|
|
141699
|
+
const comb = r.comb !== void 0 ? combFormat(draft, r.comb) : null;
|
|
141278
141700
|
outcomes.push({
|
|
141279
141701
|
op_index: i2,
|
|
141280
141702
|
op: name,
|
|
141281
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 } : {},
|
|
141282
141707
|
...noop ? { noop: true } : {}
|
|
141283
141708
|
});
|
|
141284
141709
|
cur = draft;
|
|
141285
141710
|
applied++;
|
|
141711
|
+
confirmed.push(...vouched);
|
|
141286
141712
|
counts[name] = (counts[name] ?? 0) + 1;
|
|
141287
141713
|
if (r.added) added.push({ op_index: i2, id: r.added });
|
|
141288
141714
|
if (r.geometry) placed.set(i2, r.geometry);
|
|
@@ -141324,16 +141750,17 @@ function applyPass(render, ops, excluded, screen, opts) {
|
|
|
141324
141750
|
applied,
|
|
141325
141751
|
rejected,
|
|
141326
141752
|
diff_summary: { counts, added, deleted, touched, ops: outcomes },
|
|
141327
|
-
warnings
|
|
141753
|
+
warnings,
|
|
141754
|
+
screened: { told, confirmed }
|
|
141328
141755
|
},
|
|
141329
141756
|
offPage
|
|
141330
141757
|
};
|
|
141331
141758
|
}
|
|
141332
141759
|
function wordingOf(s) {
|
|
141333
141760
|
const out = [];
|
|
141334
|
-
const add = (id, prop, v) => {
|
|
141761
|
+
const add = (id, prop, v, page) => {
|
|
141335
141762
|
if (typeof v === "string" && v.trim() && typeof id === "string")
|
|
141336
|
-
out.push({ id, prop, text: v });
|
|
141763
|
+
out.push({ id, ...typeof page === "number" ? { page } : {}, prop, text: v });
|
|
141337
141764
|
};
|
|
141338
141765
|
const rec = (x2) => typeof x2 === "object" && x2 !== null && !Array.isArray(x2) ? x2 : null;
|
|
141339
141766
|
for (const f3 of s.fields) {
|
|
@@ -141353,26 +141780,36 @@ function wordingOf(s) {
|
|
|
141353
141780
|
for (const p4 of s.pages) {
|
|
141354
141781
|
const secs = Array.isArray(p4.sections) ? p4.sections : [];
|
|
141355
141782
|
for (const sec of secs)
|
|
141356
|
-
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);
|
|
141357
141784
|
}
|
|
141358
141785
|
return out;
|
|
141359
141786
|
}
|
|
141360
141787
|
function changedWording(prev, next) {
|
|
141361
|
-
const
|
|
141362
|
-
|
|
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)));
|
|
141363
141791
|
}
|
|
141364
141792
|
function wordingChanges(changed) {
|
|
141365
141793
|
const by = /* @__PURE__ */ new Map();
|
|
141366
141794
|
for (const w of changed) {
|
|
141367
|
-
const
|
|
141368
|
-
|
|
141369
|
-
|
|
141370
|
-
|
|
141371
|
-
|
|
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
|
+
}));
|
|
141372
141808
|
}
|
|
141373
|
-
function describeWording(
|
|
141374
|
-
|
|
141375
|
-
|
|
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(", ");
|
|
141376
141813
|
}
|
|
141377
141814
|
|
|
141378
141815
|
// src/render.ts
|
|
@@ -141453,14 +141890,14 @@ async function withPage(pdf, page, fn5) {
|
|
|
141453
141890
|
p4.wasmExports.free(ptr);
|
|
141454
141891
|
}
|
|
141455
141892
|
}
|
|
141456
|
-
async function renderPage(pdf, page, dpi, crop) {
|
|
141893
|
+
async function renderPage(pdf, page, dpi, crop, maxSide = MAX_SIDE_PX) {
|
|
141457
141894
|
return withPage(pdf, page, (m, pg, heap) => {
|
|
141458
141895
|
const wPt = m.FPDF_GetPageWidthF(pg);
|
|
141459
141896
|
const hPt = m.FPDF_GetPageHeightF(pg);
|
|
141460
141897
|
const c2 = crop ?? { x: 0, y: 0, w: 1, h: 1 };
|
|
141461
141898
|
let scale2 = dpi / 72;
|
|
141462
141899
|
const longest = Math.max(wPt * c2.w, hPt * c2.h) * scale2;
|
|
141463
|
-
if (longest >
|
|
141900
|
+
if (longest > maxSide) scale2 *= maxSide / longest;
|
|
141464
141901
|
const fullW = Math.max(1, Math.round(wPt * scale2));
|
|
141465
141902
|
const fullH = Math.max(1, Math.round(hPt * scale2));
|
|
141466
141903
|
const x0 = Math.round(c2.x * fullW);
|
|
@@ -141489,7 +141926,7 @@ async function renderPage(pdf, page, dpi, crop) {
|
|
|
141489
141926
|
const row = buf + y * stride;
|
|
141490
141927
|
rgba.set(h8.subarray(row, row + width * 4), y * width * 4);
|
|
141491
141928
|
}
|
|
141492
|
-
return { width, height, rgba };
|
|
141929
|
+
return { width, height, rgba, scale: scale2 };
|
|
141493
141930
|
} finally {
|
|
141494
141931
|
m.FPDFBitmap_Destroy(bmp);
|
|
141495
141932
|
}
|
|
@@ -141926,10 +142363,10 @@ var FONT_MIN_PT = 3;
|
|
|
141926
142363
|
var OVERLAP_MIN_CHARS = 2;
|
|
141927
142364
|
var CELL_TOL = 25e-4;
|
|
141928
142365
|
var BLANK_CHARS = /^[_.\-‐-―…·•‧⋯─━┄┅┈┉_]$/u;
|
|
141929
|
-
var multiline = (f3) =>
|
|
142366
|
+
var multiline = (f3) => isRec2(f3.format) && f3.format.variant === "multiline";
|
|
141930
142367
|
var singleLine = (f3) => f3.type === "text" && !multiline(f3) || f3.type === "date" && !(f3.cells && f3.cells.length > 1);
|
|
141931
142368
|
function pinned(f3) {
|
|
141932
|
-
const s =
|
|
142369
|
+
const s = isRec2(f3.format) ? f3.format.font_size : void 0;
|
|
141933
142370
|
return typeof s === "number" ? s : null;
|
|
141934
142371
|
}
|
|
141935
142372
|
function stampedSize(f3, pageHeightPt) {
|
|
@@ -142055,7 +142492,9 @@ async function placementWarnings2(pdf, render, ids, opts = {}) {
|
|
|
142055
142492
|
}
|
|
142056
142493
|
|
|
142057
142494
|
// src/preview.ts
|
|
142495
|
+
var OVERLAYS = ["full", "outline", "none"];
|
|
142058
142496
|
var MAX_ZOOM = 8;
|
|
142497
|
+
var PREVIEW_MAX_SIDE_PX = 2e3;
|
|
142059
142498
|
var r4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
142060
142499
|
function boxOf(fs) {
|
|
142061
142500
|
if (!fs.length) return null;
|
|
@@ -142066,6 +142505,16 @@ function boxOf(fs) {
|
|
|
142066
142505
|
return { x: r4(x0 / 100), y: r4(y0 / 100), w: r4((x1 - x0) / 100), h: r4((y1 - y0) / 100) };
|
|
142067
142506
|
}
|
|
142068
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
|
+
}
|
|
142069
142518
|
function legendRows(render, page) {
|
|
142070
142519
|
const schema = normalizeSchema(render);
|
|
142071
142520
|
const entries = agentEntries(schema);
|
|
@@ -142201,6 +142650,7 @@ function legendChanges(before, now) {
|
|
|
142201
142650
|
}
|
|
142202
142651
|
async function previewPage(a) {
|
|
142203
142652
|
const dpi = Math.min(MAX_DPI, Math.max(MIN_DPI, Math.round(a.dpi ?? DEFAULT_DPI)));
|
|
142653
|
+
const overlay = a.overlay ?? "full";
|
|
142204
142654
|
const schema = normalizeSchema(a.render);
|
|
142205
142655
|
const pages = schema.pages.map((p4) => p4.page);
|
|
142206
142656
|
if (!pages.includes(a.page)) {
|
|
@@ -142218,17 +142668,20 @@ async function previewPage(a) {
|
|
|
142218
142668
|
branding: null,
|
|
142219
142669
|
logoBytes: null,
|
|
142220
142670
|
loadImage,
|
|
142221
|
-
|
|
142671
|
+
// "none" shows the stamped PDF itself: no check copy is built, so none can fail.
|
|
142672
|
+
check: overlay !== "none",
|
|
142222
142673
|
// preview_page's JSON carries the colour key; the banner would only
|
|
142223
142674
|
// cover the top of page 1.
|
|
142224
|
-
checkBanner: false
|
|
142675
|
+
checkBanner: false,
|
|
142676
|
+
checkTags: overlay === "full"
|
|
142225
142677
|
});
|
|
142226
142678
|
} catch (e4) {
|
|
142227
142679
|
if (isPasswordError(e4)) throw passwordError();
|
|
142228
142680
|
if (isFillForbiddenError(e4)) throw fillForbiddenError();
|
|
142229
142681
|
throw e4;
|
|
142230
142682
|
}
|
|
142231
|
-
|
|
142683
|
+
const shown = overlay === "none" ? res.pdf : res.checkPdf;
|
|
142684
|
+
if (!shown) {
|
|
142232
142685
|
const err2 = res.checkError;
|
|
142233
142686
|
throw new ToolError("check_copy_failed", checkCopyFailed(err2), {
|
|
142234
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.",
|
|
@@ -142237,14 +142690,19 @@ async function previewPage(a) {
|
|
|
142237
142690
|
}
|
|
142238
142691
|
const legend = legendRows(a.render, a.page);
|
|
142239
142692
|
const ids = new Set(legend.map((r) => r.id));
|
|
142240
|
-
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
|
+
});
|
|
142241
142699
|
const warnings = res.warnings.filter((w) => {
|
|
142242
142700
|
const head = w.slice(0, Math.max(0, w.indexOf(":")));
|
|
142243
142701
|
return head.split(" and ").some((id) => ids.has(topId2(id.trim())));
|
|
142244
142702
|
});
|
|
142245
142703
|
const zoom = a.crop ? Math.min(MAX_ZOOM, Math.max(1, a.zoom ?? 2)) : 1;
|
|
142246
|
-
const raster = await renderPage(
|
|
142247
|
-
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) => {
|
|
142248
142706
|
const c2 = a.crop;
|
|
142249
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;
|
|
142250
142708
|
}) : legend;
|
|
@@ -142252,8 +142710,10 @@ async function previewPage(a) {
|
|
|
142252
142710
|
png: encodePng2(raster),
|
|
142253
142711
|
width: raster.width,
|
|
142254
142712
|
height: raster.height,
|
|
142255
|
-
|
|
142256
|
-
|
|
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,
|
|
142257
142717
|
filled: res.filled,
|
|
142258
142718
|
skipped,
|
|
142259
142719
|
warnings
|
|
@@ -142818,6 +143278,52 @@ async function sweepCache(cache3, now) {
|
|
|
142818
143278
|
return removed;
|
|
142819
143279
|
}
|
|
142820
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
|
+
|
|
142821
143327
|
// src/server.ts
|
|
142822
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.`;
|
|
142823
143329
|
var INSTRUCTIONS = [
|
|
@@ -142830,7 +143336,9 @@ var INSTRUCTIONS = [
|
|
|
142830
143336
|
].join(" ");
|
|
142831
143337
|
var TEMPLATE_ID = external_exports.string().regex(/^[A-Za-z0-9-]{1,64}$/, "a fenfill template id").describe("Template id (from list_templates).");
|
|
142832
143338
|
var PAGES = external_exports.string().max(2e3).optional().describe('1-based pages, e.g. "1-3,5". Omit for all.');
|
|
142833
|
-
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
|
+
);
|
|
142834
143342
|
var OUTPUT_PATH = external_exports.string().min(1).max(4096).describe("Absolute path (or ~/\u2026) of the new .pdf to write.");
|
|
142835
143343
|
var OVERWRITE = external_exports.boolean().optional().describe("Replace output_path if it exists.");
|
|
142836
143344
|
var TARGET_PATH = external_exports.string().min(1).max(4096).optional().describe(
|
|
@@ -142877,7 +143385,14 @@ async function refreshSavedAnalyses(cache3, templateId, render, saved, who) {
|
|
|
142877
143385
|
const agent = saved.result && Array.isArray(saved.result.fields) ? saved.result : e4.agent;
|
|
142878
143386
|
await cache3.saveAnalysis({
|
|
142879
143387
|
...e4,
|
|
142880
|
-
|
|
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
|
+
},
|
|
142881
143396
|
agent: { ...agent, template_id: templateId, updated_at: saved.updated_at }
|
|
142882
143397
|
}).catch(() => void 0);
|
|
142883
143398
|
}
|
|
@@ -142934,6 +143449,19 @@ function createFenfillServer(opts = {}) {
|
|
|
142934
143449
|
};
|
|
142935
143450
|
maybeSweep();
|
|
142936
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();
|
|
142937
143465
|
const lastLegend = /* @__PURE__ */ new Map();
|
|
142938
143466
|
const fileForm = async (path) => {
|
|
142939
143467
|
const { abs: abs2, bytes } = await readInputPdf(path);
|
|
@@ -143009,6 +143537,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143009
143537
|
log("info", "tool ok", { tool: name, ms: Date.now() - t0 });
|
|
143010
143538
|
return ok(result);
|
|
143011
143539
|
} catch (e4) {
|
|
143540
|
+
untaken(args);
|
|
143012
143541
|
const err2 = asToolError(e4);
|
|
143013
143542
|
log("warn", "tool failed", { tool: name, code: err2.code, ms: Date.now() - t0 });
|
|
143014
143543
|
return fail(err2);
|
|
@@ -143109,11 +143638,11 @@ function createFenfillServer(opts = {}) {
|
|
|
143109
143638
|
const templateId = soleTemplateId(merged, null, await identity2(false));
|
|
143110
143639
|
const wc = await fileEdits({ sha, templateId });
|
|
143111
143640
|
const render = wc ? wc.render : merged.render;
|
|
143112
|
-
|
|
143641
|
+
const answers = taken(render, values2);
|
|
143113
143642
|
const res = await stampAndWrite({
|
|
143114
143643
|
pdfBytes: bytes,
|
|
143115
143644
|
render,
|
|
143116
|
-
agentInput:
|
|
143645
|
+
agentInput: answers,
|
|
143117
143646
|
fonts: loadFonts(opts.fontsDir),
|
|
143118
143647
|
branding: null,
|
|
143119
143648
|
logoBytes: null,
|
|
@@ -143171,7 +143700,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143171
143700
|
}
|
|
143172
143701
|
render = tpl.render;
|
|
143173
143702
|
}
|
|
143174
|
-
|
|
143703
|
+
const answers = taken(render, values2);
|
|
143175
143704
|
const { body: account } = await client.getJson("/account", { signal, deadline });
|
|
143176
143705
|
const blank = await templateBlank(template_id, signal, deadline);
|
|
143177
143706
|
let logo = null;
|
|
@@ -143187,7 +143716,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143187
143716
|
const res = await stampAndWrite({
|
|
143188
143717
|
pdfBytes: blank,
|
|
143189
143718
|
render,
|
|
143190
|
-
agentInput:
|
|
143719
|
+
agentInput: answers,
|
|
143191
143720
|
fonts: loadFonts(opts.fontsDir),
|
|
143192
143721
|
branding,
|
|
143193
143722
|
logoBytes: logo ? toArrayBuffer(logo) : null,
|
|
@@ -143303,24 +143832,31 @@ function createFenfillServer(opts = {}) {
|
|
|
143303
143832
|
"preview_page",
|
|
143304
143833
|
{
|
|
143305
143834
|
title: "Preview a page with its field boxes",
|
|
143306
|
-
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.`,
|
|
143307
143836
|
inputSchema: {
|
|
143308
143837
|
path: TARGET_PATH,
|
|
143309
143838
|
template_id: TARGET_TEMPLATE,
|
|
143310
143839
|
page: external_exports.number().int().min(1).max(1e5).describe("1-based page number."),
|
|
143311
|
-
values: VALUES.optional().describe(
|
|
143312
|
-
|
|
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
|
+
),
|
|
143313
143846
|
crop: external_exports.object({
|
|
143314
143847
|
x: external_exports.number().min(0).max(1),
|
|
143315
143848
|
y: external_exports.number().min(0).max(1),
|
|
143316
143849
|
w: external_exports.number().gt(0).max(1),
|
|
143317
143850
|
h: external_exports.number().gt(0).max(1)
|
|
143318
143851
|
}).optional().describe(
|
|
143319
|
-
"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."
|
|
143320
143853
|
),
|
|
143321
143854
|
zoom: external_exports.number().min(1).max(MAX_ZOOM).optional().describe(`With crop: magnification (default 2, max ${String(MAX_ZOOM)}).`),
|
|
143322
143855
|
legend: external_exports.enum(["all", "changed", "none"]).optional().describe(
|
|
143323
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.'
|
|
143324
143860
|
)
|
|
143325
143861
|
},
|
|
143326
143862
|
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
@@ -143350,16 +143886,17 @@ function createFenfillServer(opts = {}) {
|
|
|
143350
143886
|
render = wc ? wc.render : (await fetchTemplate(id, signal)).render;
|
|
143351
143887
|
bytes = await templateBlank(id, signal);
|
|
143352
143888
|
}
|
|
143353
|
-
|
|
143889
|
+
const answers = taken(render, values2);
|
|
143354
143890
|
const pv = await previewPage({
|
|
143355
143891
|
pdfBytes: bytes,
|
|
143356
143892
|
render,
|
|
143357
143893
|
page: args.page,
|
|
143358
|
-
values:
|
|
143894
|
+
values: answers,
|
|
143359
143895
|
dpi: args.dpi,
|
|
143360
143896
|
fonts: loadFonts(opts.fontsDir),
|
|
143361
143897
|
crop: args.crop,
|
|
143362
|
-
zoom: args.zoom
|
|
143898
|
+
zoom: args.zoom,
|
|
143899
|
+
overlay: args.overlay
|
|
143363
143900
|
});
|
|
143364
143901
|
const placement = await placementWarnings2(
|
|
143365
143902
|
bytes,
|
|
@@ -143392,7 +143929,13 @@ function createFenfillServer(opts = {}) {
|
|
|
143392
143929
|
edited: false,
|
|
143393
143930
|
source: args.path ? "analysis (no local edits)" : "saved template (no local edits)"
|
|
143394
143931
|
},
|
|
143395
|
-
|
|
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
|
+
},
|
|
143396
143939
|
...args.crop ? { crop: args.crop } : {},
|
|
143397
143940
|
...mode === "none" ? {} : changes ? {
|
|
143398
143941
|
legend: changes.changed,
|
|
@@ -143414,6 +143957,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143414
143957
|
]
|
|
143415
143958
|
};
|
|
143416
143959
|
} catch (e4) {
|
|
143960
|
+
untaken(args);
|
|
143417
143961
|
const err2 = asToolError(e4);
|
|
143418
143962
|
log("warn", "tool failed", { tool: "preview_page", code: err2.code, ms: Date.now() - t0 });
|
|
143419
143963
|
return fail(err2);
|
|
@@ -143428,10 +143972,15 @@ function createFenfillServer(opts = {}) {
|
|
|
143428
143972
|
inputSchema: {
|
|
143429
143973
|
path: TARGET_PATH,
|
|
143430
143974
|
template_id: TARGET_TEMPLATE,
|
|
143431
|
-
ops: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(MAX_OPS).describe(
|
|
143432
|
-
`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.`
|
|
143433
143977
|
),
|
|
143434
|
-
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
|
+
)
|
|
143435
143984
|
},
|
|
143436
143985
|
annotations: {
|
|
143437
143986
|
readOnlyHint: false,
|
|
@@ -143440,8 +143989,15 @@ function createFenfillServer(opts = {}) {
|
|
|
143440
143989
|
openWorldHint: true
|
|
143441
143990
|
}
|
|
143442
143991
|
},
|
|
143443
|
-
run("edit_template", async (
|
|
143992
|
+
run("edit_template", async (args, _extra, signal) => {
|
|
143993
|
+
const { path, template_id, ops: given, discard, confirm_wording } = args;
|
|
143444
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 ?? [];
|
|
143445
144001
|
const who = await identity2(false);
|
|
143446
144002
|
let key;
|
|
143447
144003
|
let saveTarget;
|
|
@@ -143472,6 +144028,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143472
144028
|
if (discard === true) discarded = await cache3.deleteEdits(key);
|
|
143473
144029
|
let wc = await cache3.readEdits(key);
|
|
143474
144030
|
if (wc && saveTarget && !sameAccount(wc, who)) wc = null;
|
|
144031
|
+
const had = wc !== null;
|
|
143475
144032
|
const nowIso = new Date(now()).toISOString();
|
|
143476
144033
|
if (!wc && ops.length === 0) {
|
|
143477
144034
|
return {
|
|
@@ -143513,7 +144070,10 @@ function createFenfillServer(opts = {}) {
|
|
|
143513
144070
|
snap = () => ({ none: `no page render (${why})` });
|
|
143514
144071
|
}
|
|
143515
144072
|
}
|
|
143516
|
-
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
|
+
});
|
|
143517
144077
|
let placement = [];
|
|
143518
144078
|
if (r.applied > 0) {
|
|
143519
144079
|
const ids = [
|
|
@@ -143531,14 +144091,16 @@ function createFenfillServer(opts = {}) {
|
|
|
143531
144091
|
writer,
|
|
143532
144092
|
...inherited ? { inherited_wording: true } : {}
|
|
143533
144093
|
};
|
|
143534
|
-
const
|
|
143535
|
-
if (
|
|
144094
|
+
const changed = r.applied > 0 && JSON.stringify(r.render) !== JSON.stringify(wc.render);
|
|
144095
|
+
if (changed) {
|
|
143536
144096
|
await cache3.writeEdits(out);
|
|
143537
|
-
if (saveTarget && !
|
|
144097
|
+
if (saveTarget && !had) {
|
|
143538
144098
|
await dropFileCopies(saveTarget, fileSha).catch(() => void 0);
|
|
143539
144099
|
}
|
|
143540
144100
|
}
|
|
143541
|
-
|
|
144101
|
+
echo.tell(r.screened.told);
|
|
144102
|
+
echo.confirm(r.screened.confirmed);
|
|
144103
|
+
const hasEdits = changed || had;
|
|
143542
144104
|
const left = hasEdits ? schemaIssues(out.render, 10) : [];
|
|
143543
144105
|
return {
|
|
143544
144106
|
target: saveTarget ? { template_id: saveTarget } : { path },
|
|
@@ -143554,7 +144116,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143554
144116
|
)
|
|
143555
144117
|
],
|
|
143556
144118
|
has_edits: hasEdits,
|
|
143557
|
-
ops_applied_total: out.ops_applied,
|
|
144119
|
+
ops_applied_total: changed ? out.ops_applied : wc.ops_applied,
|
|
143558
144120
|
saveable: !!saveTarget,
|
|
143559
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."
|
|
143560
144122
|
};
|
|
@@ -143564,11 +144126,11 @@ function createFenfillServer(opts = {}) {
|
|
|
143564
144126
|
"save_template",
|
|
143565
144127
|
{
|
|
143566
144128
|
title: "Save local edits to a template",
|
|
143567
|
-
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.",
|
|
143568
144130
|
inputSchema: {
|
|
143569
144131
|
template_id: TEMPLATE_ID,
|
|
143570
144132
|
confirm_wording: external_exports.boolean().optional().describe(
|
|
143571
|
-
"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."
|
|
143572
144134
|
)
|
|
143573
144135
|
},
|
|
143574
144136
|
annotations: {
|
|
@@ -143589,19 +144151,29 @@ function createFenfillServer(opts = {}) {
|
|
|
143589
144151
|
};
|
|
143590
144152
|
}
|
|
143591
144153
|
const changed = changedWording(wc.base, wc.render);
|
|
143592
|
-
const echoed = echo.check(changed);
|
|
143593
|
-
|
|
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);
|
|
143594
144162
|
throw new ToolError(
|
|
143595
144163
|
"answer_in_template",
|
|
143596
|
-
"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.",
|
|
143597
144165
|
{
|
|
143598
|
-
hint: "
|
|
143599
|
-
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
|
+
}
|
|
143600
144172
|
}
|
|
143601
144173
|
);
|
|
143602
144174
|
}
|
|
143603
|
-
|
|
143604
|
-
|
|
144175
|
+
if (unvouched && !(confirm_wording === true && listedForeign.get(wc.key) === listing)) {
|
|
144176
|
+
listedForeign.set(wc.key, listing);
|
|
143605
144177
|
throw new ToolError(
|
|
143606
144178
|
"confirm_wording_needed",
|
|
143607
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.",
|
|
@@ -143631,7 +144203,13 @@ function createFenfillServer(opts = {}) {
|
|
|
143631
144203
|
}
|
|
143632
144204
|
);
|
|
143633
144205
|
}
|
|
143634
|
-
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
|
+
};
|
|
143635
144213
|
let saved;
|
|
143636
144214
|
try {
|
|
143637
144215
|
saved = await api().patchJson(
|
|
@@ -143673,6 +144251,7 @@ function createFenfillServer(opts = {}) {
|
|
|
143673
144251
|
throw e4;
|
|
143674
144252
|
}
|
|
143675
144253
|
await cache3.deleteEdits(wc.key);
|
|
144254
|
+
listedForeign.delete(wc.key);
|
|
143676
144255
|
await dropFileCopies(template_id, wc.sha256).catch(() => void 0);
|
|
143677
144256
|
await refreshSavedAnalyses(cache3, template_id, schema, saved, await identity2(false));
|
|
143678
144257
|
return {
|
|
@@ -143690,11 +144269,18 @@ function createFenfillServer(opts = {}) {
|
|
|
143690
144269
|
return {};
|
|
143691
144270
|
}
|
|
143692
144271
|
})(),
|
|
143693
|
-
...
|
|
143694
|
-
|
|
143695
|
-
|
|
143696
|
-
|
|
143697
|
-
|
|
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
|
+
})(),
|
|
143698
144284
|
next: "fill_template, get_template and recipient links now use the saved layout."
|
|
143699
144285
|
};
|
|
143700
144286
|
})
|