@worm-vue3-print/core 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  TilingError: () => TilingError,
41
41
  WATERMARK_DEFAULTS: () => WATERMARK_DEFAULTS,
42
42
  WATERMARK_DENSITY_PRESETS: () => WATERMARK_DENSITY_PRESETS,
43
+ addNumbers: () => addNumbers,
43
44
  applyTextFitSizes: () => applyTextFitSizes,
44
45
  avg: () => avg,
45
46
  bindData: () => bindData,
@@ -64,6 +65,7 @@ __export(index_exports, {
64
65
  createCollectingCodeRenderer: () => createCollectingCodeRenderer,
65
66
  createDomHostRuntime: () => createDomHostRuntime,
66
67
  createMapCodeRenderer: () => createMapCodeRenderer,
68
+ divideNumbers: () => divideNumbers,
67
69
  elementPositionStyle: () => elementPositionStyle,
68
70
  escapeHeightMm: () => escapeHeightMm,
69
71
  escapeInlineStyleValue: () => escapeInlineStyleValue,
@@ -91,6 +93,7 @@ __export(index_exports, {
91
93
  min: () => min,
92
94
  mm: () => mm,
93
95
  mmToPx: () => mmToPx,
96
+ multiplyNumbers: () => multiplyNumbers,
94
97
  normalizeMeasurements: () => normalizeMeasurements,
95
98
  normalizePrintData: () => normalizePrintData,
96
99
  normalizeTemplate: () => normalizeTemplate,
@@ -115,11 +118,16 @@ __export(index_exports, {
115
118
  resolveSystemVariables: () => resolveSystemVariables,
116
119
  resolveWatermarkLayout: () => resolveWatermarkLayout,
117
120
  resolveWatermarkText: () => resolveWatermarkText,
121
+ round: () => round,
122
+ roundDown: () => roundDown,
118
123
  roundFontSize: () => roundFontSize,
124
+ roundHalfEven: () => roundHalfEven,
119
125
  roundMm: () => roundMm,
126
+ roundUp: () => roundUp,
120
127
  safeEval: () => safeEval,
121
128
  setPageIndex: () => setPageIndex,
122
129
  setTotalPages: () => setTotalPages,
130
+ subtractNumbers: () => subtractNumbers,
123
131
  sum: () => sum,
124
132
  systemVars: () => systemVars,
125
133
  tableDesignBottom: () => tableDesignBottom,
@@ -477,6 +485,113 @@ var Parser = class {
477
485
  }
478
486
  };
479
487
 
488
+ // src/numeric.ts
489
+ var MAX_DIGITS = 20;
490
+ var EXTRA_SCALE = 8;
491
+ function toNumber(value, fallback = 0) {
492
+ if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
493
+ if (typeof value === "string") {
494
+ const s = value.trim();
495
+ if (s === "") return fallback;
496
+ const n = Number(s);
497
+ return Number.isFinite(n) ? n : fallback;
498
+ }
499
+ if (value == null) return fallback;
500
+ if (typeof value === "boolean") return value ? 1 : 0;
501
+ return fallback;
502
+ }
503
+ function isNumericLike(value) {
504
+ if (typeof value === "number") return Number.isFinite(value);
505
+ if (typeof value === "string") {
506
+ const s = value.trim();
507
+ return s !== "" && Number.isFinite(Number(s));
508
+ }
509
+ return false;
510
+ }
511
+ function normalizeFloat(value) {
512
+ if (!Number.isFinite(value) || Number.isInteger(value)) return value;
513
+ if (Math.abs(value) >= 1e15) return value;
514
+ return Number(value.toPrecision(12));
515
+ }
516
+ function decimalsOf(n) {
517
+ if (!Number.isFinite(n) || Number.isInteger(n)) return 0;
518
+ const s = String(n);
519
+ if (s.includes("e") || s.includes("E")) return 0;
520
+ const i = s.indexOf(".");
521
+ return i === -1 ? 0 : s.length - i - 1;
522
+ }
523
+ function rescale(value, decimals) {
524
+ if (!Number.isFinite(value)) return 0;
525
+ const d = Math.min(Math.max(decimals, 0), 12);
526
+ if (d === 0) return Math.round(value);
527
+ const f = 10 ** d;
528
+ const scaled = value * f;
529
+ if (!Number.isFinite(scaled) || Math.abs(scaled) >= 1e15) return normalizeFloat(value);
530
+ return Math.round(scaled) / f;
531
+ }
532
+ function decimalAdd(left, right) {
533
+ const a = toNumber(left);
534
+ const b = toNumber(right);
535
+ const raw = a + b;
536
+ return rescale(raw, Math.max(decimalsOf(a), decimalsOf(b)));
537
+ }
538
+ function decimalSubtract(left, right) {
539
+ const a = toNumber(left);
540
+ const b = toNumber(right);
541
+ const raw = a - b;
542
+ return rescale(raw, Math.max(decimalsOf(a), decimalsOf(b)));
543
+ }
544
+ function decimalMultiply(left, right) {
545
+ const a = toNumber(left);
546
+ const b = toNumber(right);
547
+ const raw = a * b;
548
+ return rescale(raw, decimalsOf(a) + decimalsOf(b));
549
+ }
550
+ function decimalDivide(left, right) {
551
+ const a = toNumber(left);
552
+ const b = toNumber(right);
553
+ if (b === 0) return 0;
554
+ return normalizeFloat(a / b);
555
+ }
556
+ function decimalModulo(left, right) {
557
+ const a = toNumber(left);
558
+ const b = toNumber(right);
559
+ if (b === 0) return 0;
560
+ return normalizeFloat(a % b);
561
+ }
562
+ function normalizeDigits(digits) {
563
+ if (digits === void 0 || digits === null) return 2;
564
+ const d = Math.trunc(toNumber(digits, 2));
565
+ if (!Number.isFinite(d)) return 2;
566
+ return Math.min(Math.max(d, -MAX_DIGITS), MAX_DIGITS);
567
+ }
568
+ function roundTo(value, digits, mode) {
569
+ const n = toNumber(value, 0);
570
+ const d = normalizeDigits(digits);
571
+ if (n === 0) return 0;
572
+ if (Math.abs(n) >= 1e21) return normalizeFloat(n);
573
+ const scale = Math.max(d, 0) + EXTRA_SCALE;
574
+ const fixed = n.toFixed(scale);
575
+ if (!/^-?\d+(\.\d+)?$/.test(fixed)) return normalizeFloat(n);
576
+ const negative = fixed.startsWith("-");
577
+ const digitsStr = fixed.replace("-", "").replace(".", "");
578
+ let scaled = BigInt(digitsStr || "0");
579
+ if (negative) scaled = -scaled;
580
+ const pow10 = 10n ** BigInt(scale - d);
581
+ const sign = scaled < 0n ? -1n : 1n;
582
+ const abs = scaled < 0n ? -scaled : scaled;
583
+ let q = abs / pow10;
584
+ const r = abs % pow10;
585
+ const doubled = 2n * r;
586
+ if (mode === "up") {
587
+ if (r !== 0n) q += 1n;
588
+ } else if (mode !== "down" && doubled >= pow10) {
589
+ if (doubled > pow10 || mode === "half-up" || q % 2n === 1n) q += 1n;
590
+ }
591
+ const result = sign * q;
592
+ return normalizeFloat(d >= 0 ? Number(result) / 10 ** d : Number(result) * 10 ** -d);
593
+ }
594
+
480
595
  // src/evaluator.ts
481
596
  var SAFE_GLOBALS = {
482
597
  Math,
@@ -542,16 +657,17 @@ function evalBinary(node, context, functions) {
542
657
  const left = evaluate(node.left, context, functions);
543
658
  const right = evaluate(node.right, context, functions);
544
659
  switch (node.op) {
660
+ // 数值运算统一走十进制精确实现:消除 0.1 + 0.2 类浮点噪声、字符串数字按数值处理、除零兜底 0
545
661
  case "+":
546
- return left + right;
662
+ return isNumericOperands(left, right) ? decimalAdd(left, right) : concatOperands(left, right);
547
663
  case "-":
548
- return left - right;
664
+ return decimalSubtract(left, right);
549
665
  case "*":
550
- return left * right;
666
+ return decimalMultiply(left, right);
551
667
  case "/":
552
- return left / right;
668
+ return decimalDivide(left, right);
553
669
  case "%":
554
- return left % right;
670
+ return decimalModulo(left, right);
555
671
  case "<":
556
672
  return left < right;
557
673
  case ">":
@@ -580,15 +696,29 @@ function evalUnary(node, context, functions) {
580
696
  const arg = evaluate(node.arg, context, functions);
581
697
  switch (node.op) {
582
698
  case "-":
583
- return -arg;
699
+ return decimalSubtract(0, arg);
584
700
  case "+":
585
- return +arg;
701
+ return toNumber(arg);
586
702
  case "!":
587
703
  return !arg;
588
704
  default:
589
705
  throw new Error(`\u672A\u77E5\u4E00\u5143\u8FD0\u7B97\u7B26: ${node.op}`);
590
706
  }
591
707
  }
708
+ function concatOperands(left, right) {
709
+ return `${left == null ? "" : String(left)}${right == null ? "" : String(right)}`;
710
+ }
711
+ function isNumericOperands(left, right) {
712
+ const l = operandKind(left);
713
+ const r = operandKind(right);
714
+ if (l === "other" || r === "other") return false;
715
+ return l === "num" || r === "num";
716
+ }
717
+ function operandKind(value) {
718
+ if (value == null || value === "") return "neutral";
719
+ if (isNumericLike(value)) return "num";
720
+ return "other";
721
+ }
592
722
  function evalMember(node, context, functions) {
593
723
  const obj = evaluate(node.object, context, functions);
594
724
  if (obj == null) {
@@ -822,6 +952,34 @@ var systemVars = {
822
952
  printTime: () => Date.now()
823
953
  };
824
954
 
955
+ // src/functions/math.ts
956
+ function addNumbers(...values) {
957
+ return values.reduce((acc, v) => decimalAdd(acc, v), 0);
958
+ }
959
+ function subtractNumbers(...values) {
960
+ if (values.length === 0) return 0;
961
+ return values.slice(1).reduce((acc, v) => decimalSubtract(acc, v), decimalAdd(values[0], 0));
962
+ }
963
+ function multiplyNumbers(...values) {
964
+ if (values.length === 0) return 0;
965
+ return values.reduce((acc, v) => decimalMultiply(acc, v), 1);
966
+ }
967
+ function divideNumbers(a, b) {
968
+ return decimalDivide(a, b);
969
+ }
970
+ function round(value, digits) {
971
+ return roundTo(value, digits, "half-up");
972
+ }
973
+ function roundUp(value, digits) {
974
+ return roundTo(value, digits, "up");
975
+ }
976
+ function roundDown(value, digits) {
977
+ return roundTo(value, digits, "down");
978
+ }
979
+ function roundHalfEven(value, digits) {
980
+ return roundTo(value, digits, "half-even");
981
+ }
982
+
825
983
  // src/render/expression-eval.ts
826
984
  var RenderEngine = class {
827
985
  constructor() {
@@ -847,8 +1005,19 @@ var FORMAT_FUNCTIONS = {
847
1005
  IF: ifFn,
848
1006
  CONCAT: (...args) => args.filter((v) => v != null).map(String).join(""),
849
1007
  IFEMPTY: (v, d) => v != null && v !== "" ? String(v) : d,
850
- ROUND: (n, d) => Number(Number(n).toFixed(d)),
851
- LEN: (s) => String(s).length
1008
+ LEN: (s) => String(s).length,
1009
+ // 四则运算(与 +/-/*// 运算符同一套数值语义,供不方便写运算符的场景使用)
1010
+ ADD: addNumbers,
1011
+ SUB: subtractNumbers,
1012
+ MUL: multiplyNumbers,
1013
+ DIV: divideNumbers,
1014
+ // 数值修约
1015
+ ROUND: round,
1016
+ ROUNDUP: roundUp,
1017
+ CEIL: roundUp,
1018
+ ROUNDDOWN: roundDown,
1019
+ FLOOR: roundDown,
1020
+ ROUNDBANK: roundHalfEven
852
1021
  };
853
1022
  for (const [name, fn] of Object.entries(FORMAT_FUNCTIONS)) {
854
1023
  engine.registerFunction(name, fn);
@@ -893,7 +1062,7 @@ function evaluateTemplate(text, ctx) {
893
1062
 
894
1063
  // src/render/data-binder.ts
895
1064
  function bindData(template, printData, baseUrl, fontBaseUrl) {
896
- const data = printData ?? {};
1065
+ const data = { ...resolveSystemVariables(), ...printData ?? {} };
897
1066
  const bound = JSON.parse(JSON.stringify(template));
898
1067
  if (bound.header?.elements) {
899
1068
  bound.header.elements = bound.header.elements.map((el) => bindElement(el, data, baseUrl));
@@ -922,7 +1091,12 @@ function bindData(template, printData, baseUrl, fontBaseUrl) {
922
1091
  function bindElement(el, data, baseUrl) {
923
1092
  const cloned = { ...el, options: { ...el.options } };
924
1093
  if (typeof cloned.options.formatter === "string") {
925
- cloned.options.formatter = evaluateTemplate(cloned.options.formatter, data);
1094
+ const raw = cloned.options.formatter;
1095
+ if (referencesPageNumbers(raw)) {
1096
+ cloned.options.rawFormatter = raw;
1097
+ } else {
1098
+ cloned.options.formatter = evaluateTemplate(raw, data);
1099
+ }
926
1100
  }
927
1101
  const isImage = cloned.type === "image" || cloned.printElementType?.type === "image";
928
1102
  if (isImage && typeof cloned.options.src === "string") {
@@ -969,39 +1143,25 @@ function bindTableData(el, data) {
969
1143
  if (dataStartIdx < 0) dataStartIdx = renderRows.length;
970
1144
  const ctx = itemCtx(item);
971
1145
  renderRows.push(makeRenderRow(row, (cell) => {
972
- const formatter = cell.formatter;
973
- if (!formatter) return "";
974
- return evaluateTemplate(formatter, ctx);
1146
+ return resolveCellText(cell, ctx);
975
1147
  }));
976
1148
  dataRowCtx.push(ctx);
977
1149
  }
978
1150
  continue;
979
1151
  }
980
1152
  if (mode === "dynamic" && row.type === "subtotal") {
981
- const tpl = makeRenderRow(row, (cell) => {
982
- const formatter = cell.formatter;
983
- if (!formatter) return "";
984
- return evaluateTemplate(formatter, { rows: summaryRows, ...data });
985
- }, true);
1153
+ const tpl = makeRenderRow(row, (cell) => resolveCellText(cell, { rows: summaryRows, ...data }), true);
986
1154
  subtotalTemplates.push(tpl);
987
1155
  renderRows.push(tpl);
988
1156
  continue;
989
1157
  }
990
1158
  if (mode === "dynamic" && row.type === "summary") {
991
- const summaryRow = makeRenderRow(row, (cell) => {
992
- const formatter = cell.formatter;
993
- if (!formatter) return "";
994
- return evaluateTemplate(formatter, { rows: summaryRows, ...data });
995
- });
1159
+ const summaryRow = makeRenderRow(row, (cell) => resolveCellText(cell, { rows: summaryRows, ...data }));
996
1160
  summaryRenderRows.push(summaryRow);
997
1161
  renderRows.push(summaryRow);
998
1162
  continue;
999
1163
  }
1000
- renderRows.push(makeRenderRow(row, (cell) => {
1001
- const formatter = cell.formatter;
1002
- if (!formatter) return "";
1003
- return evaluateTemplate(formatter, data);
1004
- }));
1164
+ renderRows.push(makeRenderRow(row, (cell) => resolveCellText(cell, data)));
1005
1165
  }
1006
1166
  opts._renderRows = renderRows;
1007
1167
  opts._repeatHeaderCount = countRepeatHeader(rows);
@@ -1011,13 +1171,20 @@ function bindTableData(el, data) {
1011
1171
  opts._summaryRows = summaryRenderRows;
1012
1172
  opts._mainData = data;
1013
1173
  }
1174
+ function resolveCellText(cell, ctx) {
1175
+ const formatter = cell.formatter;
1176
+ if (!formatter) return "";
1177
+ if (referencesPageNumbers(formatter)) return formatter;
1178
+ return evaluateTemplate(formatter, ctx);
1179
+ }
1014
1180
  function makeRenderRow(row, resolve, keepRaw = false) {
1015
1181
  return {
1016
1182
  type: row.type,
1017
1183
  height: row.height ?? 8,
1018
1184
  cells: row.cells.map((cell) => ({
1019
1185
  content: cell.merged ? "" : resolve(cell),
1020
- ...keepRaw ? { rawFormatter: cell.merged ? "" : cell.formatter ?? "" } : {},
1186
+ // 小计行(keepRaw)与引用页码的单元格都保留原始表达式:前者按当页数据行重算,后者按当页页码重算
1187
+ ...(keepRaw || referencesPageNumbers(cell.formatter)) && !cell.merged ? { rawFormatter: cell.formatter ?? "" } : {},
1021
1188
  cellType: cell.cellType,
1022
1189
  barcodeType: cell.barcodeType,
1023
1190
  qrCodeLevel: cell.qrCodeLevel,
@@ -1045,16 +1212,35 @@ function makeRenderRow(row, resolve, keepRaw = false) {
1045
1212
  };
1046
1213
  }
1047
1214
  function countRepeatHeader(rows) {
1215
+ const headerCount = countLeadingHeaderRows(rows);
1216
+ if (headerCount === 0) return 0;
1217
+ const enabled = rows.slice(0, headerCount).some((row) => row.repeatOnPage === true);
1218
+ if (!enabled) return 0;
1219
+ return alignToRowspanBoundary(rows, headerCount);
1220
+ }
1221
+ function countLeadingHeaderRows(rows) {
1048
1222
  let n = 0;
1049
1223
  for (const row of rows) {
1050
- if (row.type === "header" && row.repeatOnPage === true) {
1051
- n++;
1052
- } else {
1053
- break;
1054
- }
1224
+ if (row?.type !== "header") break;
1225
+ n++;
1055
1226
  }
1056
1227
  return n;
1057
1228
  }
1229
+ function isRowspanComplete(rows, n) {
1230
+ for (let r = 0; r < n; r++) {
1231
+ for (const cell of rows[r]?.cells ?? []) {
1232
+ if (cell?.merged) continue;
1233
+ if (r + (cell?.rowspan ?? 1) > n) return false;
1234
+ }
1235
+ }
1236
+ return true;
1237
+ }
1238
+ function alignToRowspanBoundary(rows, max2) {
1239
+ for (let n = max2; n >= 1; n--) {
1240
+ if (isRowspanComplete(rows, n)) return n;
1241
+ }
1242
+ return 1;
1243
+ }
1058
1244
  var pad2 = (n) => String(n).padStart(2, "0");
1059
1245
  function resolveSystemVariables(now = /* @__PURE__ */ new Date(), page = {}) {
1060
1246
  return {
@@ -1064,6 +1250,9 @@ function resolveSystemVariables(now = /* @__PURE__ */ new Date(), page = {}) {
1064
1250
  totalPages: page.totalPages ?? 1
1065
1251
  };
1066
1252
  }
1253
+ function referencesPageNumbers(expr) {
1254
+ return typeof expr === "string" && /\b(pageIndex|totalPages)\b/.test(expr);
1255
+ }
1067
1256
  function injectSystemVariables(html, now = /* @__PURE__ */ new Date()) {
1068
1257
  const { printDate, printTime } = resolveSystemVariables(now);
1069
1258
  return html.replace(/\{printDate\}/g, printDate).replace(/\{printTime\}/g, printTime);
@@ -2144,6 +2333,13 @@ function barcodeAvailableBoxMm(boxMm, maxMm) {
2144
2333
  }
2145
2334
 
2146
2335
  // src/render/html-generator.ts
2336
+ function pageVarsContext(vars) {
2337
+ return { ...vars.data ?? {}, pageIndex: vars.pageIndex, totalPages: vars.totalPages };
2338
+ }
2339
+ function pageVarsData(printData) {
2340
+ if (Array.isArray(printData)) return printData[0] ?? {};
2341
+ return printData ?? {};
2342
+ }
2147
2343
  function generateHtml(template, pageLayouts, printData, options) {
2148
2344
  const css = buildFontFaceCss(template.fonts) + buildPageCss(template, options?.pageHeightMm);
2149
2345
  const isMeasure = options?.isMeasurementPass === true;
@@ -2228,22 +2424,25 @@ function renderPage(template, page, pageNum, totalPages, ctx, printData, pageCla
2228
2424
  height: ctx.pageHeightMm && ctx.pageHeightMm > 0 ? ctx.pageHeightMm : paper.height
2229
2425
  };
2230
2426
  const contentWidth = paper.width - template.margins.left - template.margins.right;
2427
+ const pageVars = { pageIndex: pageNum, totalPages, data: pageVarsData(printData) };
2428
+ const scoped = withPageNumbers(template, pageVars);
2429
+ const pageCtx = { ...ctx, pageVars };
2231
2430
  const headerHtml = renderAreaElements(
2232
- template.header?.elements ?? [],
2431
+ scoped.header?.elements ?? [],
2233
2432
  contentWidth,
2234
2433
  pageNum,
2235
2434
  totalPages,
2236
- ctx
2435
+ pageCtx
2237
2436
  );
2238
2437
  const footerHtml = renderAreaElements(
2239
- template.footer?.elements ?? [],
2438
+ scoped.footer?.elements ?? [],
2240
2439
  contentWidth,
2241
2440
  pageNum,
2242
2441
  totalPages,
2243
- ctx
2442
+ pageCtx
2244
2443
  );
2245
- const overlayHtml = page.pageIndex === 0 ? `<div class="first-page-overlay">${renderAreaElements(template.firstPageOverlay?.elements ?? [], contentWidth, void 0, void 0, ctx)}</div>` : "";
2246
- let contentHtml = page.sections.map((section) => renderSection(section, template, ctx)).join("\n");
2444
+ const overlayHtml = page.pageIndex === 0 ? `<div class="first-page-overlay">${renderAreaElements(scoped.firstPageOverlay?.elements ?? [], contentWidth, pageNum, totalPages, pageCtx)}</div>` : "";
2445
+ let contentHtml = page.sections.map((section) => renderSection(section, scoped, pageCtx)).join("\n");
2247
2446
  contentHtml = contentHtml.replace(/\{pageIndex\}/g, String(pageNum));
2248
2447
  contentHtml = contentHtml.replace(/\{totalPages\}/g, String(totalPages));
2249
2448
  const pageInner = `
@@ -2260,6 +2459,39 @@ function renderPage(template, page, pageNum, totalPages, ctx, printData, pageCla
2260
2459
  ${pageBody}
2261
2460
  </section>`;
2262
2461
  }
2462
+ function withPageNumbers(template, vars) {
2463
+ if (!templateReferencesPageNumbers(template)) return template;
2464
+ const context = pageVarsContext(vars);
2465
+ const mapEl = (el) => {
2466
+ const raw = el.options?.rawFormatter;
2467
+ if (typeof raw !== "string") return el;
2468
+ return { ...el, options: { ...el.options, formatter: evaluateTemplate(raw, context) } };
2469
+ };
2470
+ const mapArea = (area) => area ? { ...area, elements: (area.elements ?? []).map(mapEl) } : area;
2471
+ return {
2472
+ ...template,
2473
+ elements: template.elements.map(mapEl),
2474
+ header: mapArea(template.header),
2475
+ footer: mapArea(template.footer),
2476
+ firstPageOverlay: mapArea(template.firstPageOverlay)
2477
+ };
2478
+ }
2479
+ function templateReferencesPageNumbers(template) {
2480
+ const areas = [
2481
+ template.elements,
2482
+ template.header?.elements,
2483
+ template.footer?.elements,
2484
+ template.firstPageOverlay?.elements
2485
+ ];
2486
+ return areas.some((list) => (list ?? []).some((el) => typeof el.options?.rawFormatter === "string"));
2487
+ }
2488
+ function resolveCellContent(cell, ctx) {
2489
+ const raw = cell.rawFormatter;
2490
+ if (typeof raw === "string" && raw !== "" && ctx?.pageVars) {
2491
+ return evaluateTemplate(raw, pageVarsContext(ctx.pageVars));
2492
+ }
2493
+ return cell.content;
2494
+ }
2263
2495
  function renderSection(section, template, ctx) {
2264
2496
  const el = findElement(template, section.elementId);
2265
2497
  if (!el) {
@@ -2412,7 +2644,7 @@ function matrixCellStyle(cell, opts) {
2412
2644
  }
2413
2645
  return parts.join(";");
2414
2646
  }
2415
- function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOwner = { elementId: "", kind: "b" }) {
2647
+ function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOwner = { elementId: "", kind: "b" }, rowLimit) {
2416
2648
  const trs = [];
2417
2649
  const defaultPadding = opts.tableDefaultPadding ?? 1;
2418
2650
  for (let r = start; r < end; r++) {
@@ -2420,12 +2652,14 @@ function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOw
2420
2652
  if (!row) continue;
2421
2653
  const idxAttr = withRowIndex ? ` data-row-index="${r}"` : "";
2422
2654
  const tds = row.cells.map((cell, ci) => ({ cell, ci })).filter(({ cell }) => !cell.merged).map(({ cell, ci }) => {
2423
- const span = `${cell.rowspan > 1 ? ` rowspan="${cell.rowspan}"` : ""}${cell.colspan > 1 ? ` colspan="${cell.colspan}"` : ""}`;
2655
+ const rowspan = rowLimit === void 0 ? cell.rowspan ?? 1 : Math.max(1, Math.min(cell.rowspan ?? 1, rowLimit - r));
2656
+ const span = `${rowspan > 1 ? ` rowspan="${rowspan}"` : ""}${cell.colspan > 1 ? ` colspan="${cell.colspan}"` : ""}`;
2424
2657
  const colIndex = ci;
2425
2658
  let inner;
2426
2659
  if (cell.cellType === "barcode" || cell.cellType === "qrcode") {
2427
- inner = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;">${codeImgHtml(cell.content, cell.cellType, cell, {
2428
- fallback: esc(cell.content),
2660
+ const codeValue = resolveCellContent(cell, ctx);
2661
+ inner = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;">${codeImgHtml(codeValue, cell.cellType, cell, {
2662
+ fallback: esc(codeValue),
2429
2663
  codeRenderer: ctx?.codeRenderer,
2430
2664
  fit: cell.fit,
2431
2665
  maxWidth: cell.maxWidth,
@@ -2439,9 +2673,9 @@ function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOw
2439
2673
  const fit = cell.fit || "contain";
2440
2674
  const maxWidth = cell.maxWidth ? `max-width:${cell.maxWidth}mm;` : "max-width:100%;";
2441
2675
  const maxHeight = cell.maxHeight ? `max-height:${cell.maxHeight}mm;` : "max-height:100%;";
2442
- inner = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;"><img src="${esc(cell.content)}" style="object-fit:${fit};${maxWidth}${maxHeight}display:block;" /></div>`;
2676
+ inner = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;"><img src="${esc(resolveCellContent(cell, ctx))}" style="object-fit:${fit};${maxWidth}${maxHeight}display:block;" /></div>`;
2443
2677
  } else {
2444
- inner = renderTextCellInner(renderRows, r, colIndex, cell, opts, fitOwner, defaultPadding);
2678
+ inner = renderTextCellInner(renderRows, r, colIndex, cell, opts, fitOwner, defaultPadding, ctx);
2445
2679
  }
2446
2680
  return `<td${span} style="${matrixCellStyle(cell, opts)}">${inner}</td>`;
2447
2681
  }).join("");
@@ -2449,9 +2683,9 @@ function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOw
2449
2683
  }
2450
2684
  return trs.join("\n");
2451
2685
  }
2452
- function renderTextCellInner(renderRows, rowIndex, colIndex, cell, opts, fitOwner, defaultPadding) {
2686
+ function renderTextCellInner(renderRows, rowIndex, colIndex, cell, opts, fitOwner, defaultPadding, ctx) {
2453
2687
  const fit = resolveCellTextFit(cell);
2454
- const text = esc(cell.content);
2688
+ const text = esc(resolveCellContent(cell, ctx));
2455
2689
  if (fit === "autoHeight") return text;
2456
2690
  const capMm = cellFitCapMm(renderRows, rowIndex, cell, defaultPadding);
2457
2691
  const nowrap = cell.wordWrap === false;
@@ -2489,7 +2723,7 @@ function renderTableSlice(el, section, ctx) {
2489
2723
  const startRow = section.startRow ?? 0;
2490
2724
  const endRow = section.endRow ?? renderRows.length;
2491
2725
  const repeatCount = opts._repeatHeaderCount ?? 0;
2492
- const repeatHtml = section.repeatHeader && repeatCount > 0 ? renderMatrixRows(renderRows, 0, repeatCount, opts, false, ctx, { elementId: el.id, kind: "b" }) : "";
2726
+ const repeatHtml = section.repeatHeader && repeatCount > 0 ? renderMatrixRows(renderRows, 0, repeatCount, opts, false, ctx, { elementId: el.id, kind: "b" }, repeatCount) : "";
2493
2727
  const bodyHtml = renderMatrixRows(renderRows, startRow, endRow, opts, false, ctx, { elementId: el.id, kind: "b" });
2494
2728
  const subtotalHtml = section.subtotal ? renderSubtotalRows(el, section, opts, ctx) : "";
2495
2729
  const summaryHtml = section.summary ? renderSummaryRows(el, opts, ctx) : "";
@@ -2510,9 +2744,15 @@ function renderSubtotalRows(el, section, opts, ctx) {
2510
2744
  const dataStart = Math.max(startRow, dataStartIdx);
2511
2745
  const dataEnd = Math.max(endRow, dataStart);
2512
2746
  const pageCtx = dataEnd > dataStartIdx ? dataRowCtx.slice(Math.max(dataStart - dataStartIdx, 0), dataEnd - dataStartIdx) : [];
2747
+ const pageVars = ctx?.pageVars ? pageVarsContext(ctx.pageVars) : {};
2513
2748
  const rows = templates.map((tpl) => ({
2514
2749
  ...tpl,
2515
- cells: tpl.cells.map((cell) => cell.rawFormatter ? { ...cell, content: evaluateTemplate(cell.rawFormatter, { rows: pageCtx, ...mainData }) } : cell)
2750
+ cells: tpl.cells.map((cell) => cell.rawFormatter ? {
2751
+ ...cell,
2752
+ content: evaluateTemplate(cell.rawFormatter, { rows: pageCtx, ...mainData, ...pageVars }),
2753
+ // 已按本页上下文求值,清标记避免渲染时再按页码重算一次(会丢掉 rows)
2754
+ rawFormatter: ""
2755
+ } : cell)
2516
2756
  }));
2517
2757
  return renderMatrixRows(rows, 0, rows.length, opts, false, ctx, { elementId: el.id, kind: "st" });
2518
2758
  }
@@ -2535,7 +2775,7 @@ function renderFlowGroup(el, section, template, ctx) {
2535
2775
  if (endRow > startRow || section.subtotal || section.summary) {
2536
2776
  const renderRows = opts._renderRows ?? [];
2537
2777
  const repeatCount = opts._repeatHeaderCount ?? 0;
2538
- const repeatHtml = section.repeatHeader && repeatCount > 0 ? renderMatrixRows(renderRows, 0, repeatCount, opts, false, ctx, { elementId: el.id, kind: "b" }) : "";
2778
+ const repeatHtml = section.repeatHeader && repeatCount > 0 ? renderMatrixRows(renderRows, 0, repeatCount, opts, false, ctx, { elementId: el.id, kind: "b" }, repeatCount) : "";
2539
2779
  const bodyHtml = renderMatrixRows(renderRows, startRow, endRow, opts, false, ctx, { elementId: el.id, kind: "b" });
2540
2780
  const subtotalHtml = section.subtotal ? renderSubtotalRows(el, section, opts, ctx) : "";
2541
2781
  const summaryHtml = section.summary ? renderSummaryRows(el, opts, ctx) : "";
@@ -3576,6 +3816,7 @@ var TemplateEngine = class {
3576
3816
  TilingError,
3577
3817
  WATERMARK_DEFAULTS,
3578
3818
  WATERMARK_DENSITY_PRESETS,
3819
+ addNumbers,
3579
3820
  applyTextFitSizes,
3580
3821
  avg,
3581
3822
  bindData,
@@ -3600,6 +3841,7 @@ var TemplateEngine = class {
3600
3841
  createCollectingCodeRenderer,
3601
3842
  createDomHostRuntime,
3602
3843
  createMapCodeRenderer,
3844
+ divideNumbers,
3603
3845
  elementPositionStyle,
3604
3846
  escapeHeightMm,
3605
3847
  escapeInlineStyleValue,
@@ -3627,6 +3869,7 @@ var TemplateEngine = class {
3627
3869
  min,
3628
3870
  mm,
3629
3871
  mmToPx,
3872
+ multiplyNumbers,
3630
3873
  normalizeMeasurements,
3631
3874
  normalizePrintData,
3632
3875
  normalizeTemplate,
@@ -3651,11 +3894,16 @@ var TemplateEngine = class {
3651
3894
  resolveSystemVariables,
3652
3895
  resolveWatermarkLayout,
3653
3896
  resolveWatermarkText,
3897
+ round,
3898
+ roundDown,
3654
3899
  roundFontSize,
3900
+ roundHalfEven,
3655
3901
  roundMm,
3902
+ roundUp,
3656
3903
  safeEval,
3657
3904
  setPageIndex,
3658
3905
  setTotalPages,
3906
+ subtractNumbers,
3659
3907
  sum,
3660
3908
  systemVars,
3661
3909
  tableDesignBottom,
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { T as TemplateData, C as CodeRenderer, P as PageLayout, I as MeasuredElement, J as TemplateElement, W as WatermarkOptions, K as PaperMm, L as ViewportPx, N as HeightSource, O as MarginsMm, Q as PdfTargetSpec, X as ScreenshotTargetSpec, R as RawMeasurement, a as CodeSpec, Y as CodeRenderOptions, D as DriverFactory, Z as ExecutorBundle, _ as TileLayout, $ as PrintJob, a0 as PreparedDocument, a1 as RenderPdfResult, M as MultiPageTemplateData, a2 as PrintFontDeclaration } from './driver-CsWkGK4a.cjs';
2
- export { a3 as ContinuousPaperSize, a4 as EXECUTOR_TARGETS, a5 as ExecutorMethod, a6 as ExecutorTargetKind, a7 as FALLBACK_FONT_STACK, a8 as MAX_BATCH_COPIES, a9 as NormalizedPrintData, aa as PAPER_DIMENSIONS, ab as PageDriver, ac as PageSection, ad as PaperSize, ae as PrintDataInput, af as PrintFontFile, ag as RenderCell, ah as RenderRequest, ai as RenderRow, aj as SheetPaperSize, ak as TILE_DEFAULTS, G as TextFit, al as TilingError, am as TilingIssue, an as TilingIssueCode, ao as TilingOptions, ap as TilingResolveOptions, aq as TilingTemplateInput, ar as buildFontFaceCss, as as computeMaxColumns, at as computeTileLayout, au as escapeInlineStyleValue, av as getPaperDimensions, aw as isContinuousPaper, ax as isContinuousPaperSize, ay as normalizePrintData, az as normalizeTilingOptions, aA as resolveSheetMm, aB as roundMm, aC as tilePosition, aD as toFontFamilyStack, aE as validateTiling } from './driver-CsWkGK4a.cjs';
3
- import { F as FitFontSize, P as PrintRuntime } from './ports-q934kIC7.cjs';
4
- export { C as CellFitRowKind, h as DEFAULT_READINESS_MS, D as DEFAULT_SHRINK_MIN_FONT_SIZE_PT, i as DEFAULT_TIMEOUT_MS, M as MIN_SHRINK_FONT_SIZE_PT, j as MeasureResult, k as PrintSession, S as SessionBudget, c as cellFitCapMm, a as cellFitKey, f as floorFontSize, p as parseCellFitKey, r as resolveCellTextFit, d as resolveElementTextFit, e as resolveShrinkMinFontSize, g as roundFontSize } from './ports-q934kIC7.cjs';
1
+ import { T as TemplateData, C as CodeRenderer, P as PageLayout, I as MeasuredElement, J as TemplateElement, W as WatermarkOptions, K as PaperMm, L as ViewportPx, N as HeightSource, O as MarginsMm, Q as PdfTargetSpec, X as ScreenshotTargetSpec, R as RawMeasurement, a as CodeSpec, Y as CodeRenderOptions, D as DriverFactory, Z as ExecutorBundle, _ as TileLayout, $ as PrintJob, a0 as PreparedDocument, a1 as RenderPdfResult, M as MultiPageTemplateData, a2 as PrintFontDeclaration } from './driver-Dn_YAzO5.cjs';
2
+ export { a3 as ContinuousPaperSize, a4 as EXECUTOR_TARGETS, a5 as ExecutorMethod, a6 as ExecutorTargetKind, a7 as FALLBACK_FONT_STACK, a8 as MAX_BATCH_COPIES, a9 as NormalizedPrintData, aa as PAPER_DIMENSIONS, ab as PageDriver, ac as PageSection, ad as PaperSize, ae as PrintDataInput, af as PrintFontFile, ag as RenderCell, ah as RenderRequest, ai as RenderRow, aj as SheetPaperSize, ak as TILE_DEFAULTS, G as TextFit, al as TilingError, am as TilingIssue, an as TilingIssueCode, ao as TilingOptions, ap as TilingResolveOptions, aq as TilingTemplateInput, ar as buildFontFaceCss, as as computeMaxColumns, at as computeTileLayout, au as escapeInlineStyleValue, av as getPaperDimensions, aw as isContinuousPaper, ax as isContinuousPaperSize, ay as normalizePrintData, az as normalizeTilingOptions, aA as resolveSheetMm, aB as roundMm, aC as tilePosition, aD as toFontFamilyStack, aE as validateTiling } from './driver-Dn_YAzO5.cjs';
3
+ import { F as FitFontSize, P as PrintRuntime } from './ports-Cf5sjwls.cjs';
4
+ export { C as CellFitRowKind, h as DEFAULT_READINESS_MS, D as DEFAULT_SHRINK_MIN_FONT_SIZE_PT, i as DEFAULT_TIMEOUT_MS, M as MIN_SHRINK_FONT_SIZE_PT, j as MeasureResult, k as PrintSession, S as SessionBudget, c as cellFitCapMm, a as cellFitKey, f as floorFontSize, p as parseCellFitKey, r as resolveCellTextFit, d as resolveElementTextFit, e as resolveShrinkMinFontSize, g as roundFontSize } from './ports-Cf5sjwls.cjs';
5
5
 
6
6
  type TokenType = 'num' | 'str' | 'bool' | 'ident' | 'punc';
7
7
  interface Token {
@@ -131,6 +131,25 @@ declare const systemVars: {
131
131
  printTime: () => number;
132
132
  };
133
133
 
134
+ /** ADD(a, b, …):相加,无法解析为数字的参数按 0 处理 */
135
+ declare function addNumbers(...values: unknown[]): number;
136
+ /** SUB(a, b, …):首项依次减去后续各项 */
137
+ declare function subtractNumbers(...values: unknown[]): number;
138
+ /** MUL(a, b, …):相乘 */
139
+ declare function multiplyNumbers(...values: unknown[]): number;
140
+ /** DIV(a, b):相除,除数为 0 返回 0(不产生 Infinity / NaN) */
141
+ declare function divideNumbers(a: unknown, b: unknown): number;
142
+ /**
143
+ * ROUND(n, d):四舍五入到 d 位小数(d 缺省 2;负数修约到整十/整百,如 -2 → 百位)
144
+ */
145
+ declare function round(value: unknown, digits?: unknown): number;
146
+ /** ROUNDUP / CEIL(n, d):进一法(远离零方向) */
147
+ declare function roundUp(value: unknown, digits?: unknown): number;
148
+ /** ROUNDDOWN / FLOOR(n, d):去尾法(朝零方向) */
149
+ declare function roundDown(value: unknown, digits?: unknown): number;
150
+ /** ROUNDBANK(n, d):四舍六入五成双(GB/T 8170 数值修约规则) */
151
+ declare function roundHalfEven(value: unknown, digits?: unknown): number;
152
+
134
153
  /**
135
154
  * 将模板中元素的 formatter 表达式求值为实际值。
136
155
  * 返回新的模板对象(深拷贝),不修改原始模板。
@@ -551,4 +570,4 @@ declare class TemplateEngine {
551
570
  render(template: string, context: Record<string, any>): string;
552
571
  }
553
572
 
554
- export { type ASTNode, type BatchCopyInput, CodeRenderOptions, CodeRenderer, CodeSpec, type CollectingCodeRenderer, DriverFactory, type ElectronPrintToPdfOptions, type EngineOptions, ExecutorBundle, type ExprFunction, FitFontSize, type GenerateOptions, HeightSource, MICROMETERS_PER_INCH, MICROMETERS_PER_MM, MIN_CONTINUOUS_HEIGHT_MM, MM_PER_INCH, MM_PER_PX, MarginsMm, MeasuredElement, type MultiPageCopyInput, type MultiPageDocument, MultiPageTemplateData, PX_PER_MM, PageLayout, PaperMm, type PaperOverride, PdfTargetSpec, type PlaywrightPdfOptions, PreparedDocument, PrintFailure, type PrintFailureCode, PrintFontDeclaration, PrintJob, PrintRuntime, TemplateData as PrintTemplateData, TemplateElement as PrintTemplateElement, RawMeasurement, type RenderFinalPagesOptions, RenderPdfResult, type ResolvePaperInput, type ResolvedWatermark, ScreenshotTargetSpec, type SystemVariableContext, type TemplateAST, TemplateEngine, type TileInput, TileLayout, ViewportPx, WATERMARK_DEFAULTS, WATERMARK_DENSITY_PRESETS, WatermarkOptions, type WatermarkPaper, type WatermarkTile, applyTextFitSizes, avg, bindData, buildBasePageCss, buildPageCss, buildPageGeometryCss, buildPageRuleCss, buildPdfTargetSpec, buildScreenshotTargetSpec, codeSpecKey, compileTemplate, composeBatchHtml, composeContinuousHeight, composeMultiPageDocument, composeTiledHtml, count, createCollectingCodeRenderer, createDomHostRuntime, createMapCodeRenderer, elementPositionStyle, escapeHeightMm, evaluate, evaluateTemplate, formatDate, formatMoney, formatTimestamp, generateHtml, getByPath, ifFn, injectSystemVariables, isMultiPageTemplate, isWatermarkVisible, max, mergeCodeMaps, mergeFontDeclarations, micrometersToMillimeters, millimetersToInches, millimetersToMicrometers, min, mm, mmToPx, normalizeMeasurements, normalizeTemplate, paginate, paperViewportPx, parse, parseTemplate, prepareDocument, pxToMm, renderPdf, renderScreenshot, renderTemplate, renderWatermarkLayerHtml, renderWatermarkTileSvg, resolvePaperMm, resolveSystemVariables, resolveWatermarkLayout, resolveWatermarkText, safeEval, setPageIndex, setTotalPages, sum, systemVars, tableDesignBottom, toElectronPrintToPdfOptions, toPlaywrightPdfOptions, toPrintFailure, toUpperCaseAmount, tokenize, withTimeout };
573
+ export { type ASTNode, type BatchCopyInput, CodeRenderOptions, CodeRenderer, CodeSpec, type CollectingCodeRenderer, DriverFactory, type ElectronPrintToPdfOptions, type EngineOptions, ExecutorBundle, type ExprFunction, FitFontSize, type GenerateOptions, HeightSource, MICROMETERS_PER_INCH, MICROMETERS_PER_MM, MIN_CONTINUOUS_HEIGHT_MM, MM_PER_INCH, MM_PER_PX, MarginsMm, MeasuredElement, type MultiPageCopyInput, type MultiPageDocument, MultiPageTemplateData, PX_PER_MM, PageLayout, PaperMm, type PaperOverride, PdfTargetSpec, type PlaywrightPdfOptions, PreparedDocument, PrintFailure, type PrintFailureCode, PrintFontDeclaration, PrintJob, PrintRuntime, TemplateData as PrintTemplateData, TemplateElement as PrintTemplateElement, RawMeasurement, type RenderFinalPagesOptions, RenderPdfResult, type ResolvePaperInput, type ResolvedWatermark, ScreenshotTargetSpec, type SystemVariableContext, type TemplateAST, TemplateEngine, type TileInput, TileLayout, ViewportPx, WATERMARK_DEFAULTS, WATERMARK_DENSITY_PRESETS, WatermarkOptions, type WatermarkPaper, type WatermarkTile, addNumbers, applyTextFitSizes, avg, bindData, buildBasePageCss, buildPageCss, buildPageGeometryCss, buildPageRuleCss, buildPdfTargetSpec, buildScreenshotTargetSpec, codeSpecKey, compileTemplate, composeBatchHtml, composeContinuousHeight, composeMultiPageDocument, composeTiledHtml, count, createCollectingCodeRenderer, createDomHostRuntime, createMapCodeRenderer, divideNumbers, elementPositionStyle, escapeHeightMm, evaluate, evaluateTemplate, formatDate, formatMoney, formatTimestamp, generateHtml, getByPath, ifFn, injectSystemVariables, isMultiPageTemplate, isWatermarkVisible, max, mergeCodeMaps, mergeFontDeclarations, micrometersToMillimeters, millimetersToInches, millimetersToMicrometers, min, mm, mmToPx, multiplyNumbers, normalizeMeasurements, normalizeTemplate, paginate, paperViewportPx, parse, parseTemplate, prepareDocument, pxToMm, renderPdf, renderScreenshot, renderTemplate, renderWatermarkLayerHtml, renderWatermarkTileSvg, resolvePaperMm, resolveSystemVariables, resolveWatermarkLayout, resolveWatermarkText, round, roundDown, roundHalfEven, roundUp, safeEval, setPageIndex, setTotalPages, subtractNumbers, sum, systemVars, tableDesignBottom, toElectronPrintToPdfOptions, toPlaywrightPdfOptions, toPrintFailure, toUpperCaseAmount, tokenize, withTimeout };