@worm-vue3-print/core 1.2.2 → 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.
@@ -1,4 +1,5 @@
1
- import { P as PageLayout, T as TemplateData, C as CodeRenderer } from '../types-BGBAbQD5.cjs';
1
+ import { P as PageLayout, T as TemplateData, M as MultiPageTemplateData, C as CodeRenderer, D as DriverFactory, R as RawMeasurement, a as CodeSpec } from '../driver-Dn_YAzO5.cjs';
2
+ import { P as PrintRuntime, F as FitFontSize } from '../ports-Cf5sjwls.cjs';
2
3
 
3
4
  interface BrowserRenderResult {
4
5
  /** 最终多页 HTML 字符串 */
@@ -7,17 +8,89 @@ interface BrowserRenderResult {
7
8
  pageCount: number;
8
9
  /** 分页布局(调试/高级用途) */
9
10
  pageLayouts: PageLayout[];
11
+ /** 最终纸张尺寸(mm,已含方向);连续纸为探针推导后的高度 */
12
+ paperMm: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /** 模板是否连续纸 */
17
+ continuous: boolean;
18
+ /** 份数(对象数据=1;数组=数组长度) */
19
+ copies: number;
20
+ /** 批量时每份纸张尺寸(连续纸各份高度可能不同) */
21
+ copyPaperMm?: {
22
+ width: number;
23
+ height: number;
24
+ }[];
25
+ }
26
+ interface BrowserRenderOptions {
27
+ /** 连续纸显式纸高覆盖(mm);仅对 CONTINUOUS 生效 */
28
+ paperHeightMm?: number;
29
+ /**
30
+ * 相对路径字体基址。缺省空串 = 不拼接,由文档自身 origin 解析——
31
+ * 与设计器画布注入 @font-face 的语义一致(画布能加载的字体,预览/浏览器打印也能加载)。
32
+ * 传值则拼在该基址上,供字体与页面不同源的宿主使用。
33
+ */
34
+ fontBaseUrl?: string;
10
35
  }
11
36
  /**
12
- * 浏览器内完成「数据绑定 → 测量 → 分页 → 最终 HTML」全流程。
13
- * @param template 模板 JSON(设计器 TemplateData 结构兼容)
37
+ * 浏览器内完成「数据绑定 → 码值渲染 → 测量 → 分页 → 连续纸推导 → 最终 HTML」。
38
+ * @param template 模板 JSON(设计器 TemplateData 结构兼容;含 { pages } 多页面模板)
14
39
  * @param printData 业务数据
15
40
  * @param baseUrl 图片相对路径拼接前缀
16
- * @param codeRenderer 条码渲染器(browserCodeRenderer)
41
+ * @param codeRenderer 调用方提供的码值渲染器;传入时沿用该渲染器(既有覆盖语义)
42
+ * @param options 渲染选项(连续纸纸高覆盖等)
17
43
  */
18
- declare function renderHtmlPages(template: TemplateData, printData?: Record<string, any> | Record<string, any>[], baseUrl?: string, codeRenderer?: CodeRenderer): Promise<BrowserRenderResult>;
44
+ declare function renderHtmlPages(template: TemplateData | MultiPageTemplateData, printData?: Record<string, any> | Record<string, any>[], baseUrl?: string, codeRenderer?: CodeRenderer, options?: BrowserRenderOptions): Promise<BrowserRenderResult>;
19
45
 
20
46
  /** 浏览器条码/二维码渲染器;码值非法或为空时抛错,由渲染管线降级文本占位 */
21
47
  declare const browserCodeRenderer: CodeRenderer;
22
48
 
23
- export { type BrowserRenderResult, browserCodeRenderer, renderHtmlPages };
49
+ /** 浏览器端 runtime:iframe driver + 进程内执行器;不提供 PDF/截图 */
50
+ declare function createBrowserPrintRuntime(): PrintRuntime;
51
+
52
+ /** 浏览器 iframe driver:进程内直调执行器,无需注入脚本;不提供出图能力 */
53
+ declare function createIframeDriverFactory(): DriverFactory;
54
+
55
+ /**
56
+ * 单个节点自动缩小;返回最终字号(pt)。非 shrink 节点返回 undefined。
57
+ * 缩到下限仍放不下时保持下限字号,由容器裁剪兜底(等价退化为截断)。
58
+ * 返回的字号与写进 DOM 的字号完全一致(向下取两位小数),
59
+ * 否则测量趟按 9.623pt 排版、最终趟按回写的 9.62pt 渲染,会出现不可控的亚像素错版。
60
+ */
61
+ declare function fitTextNode(el: HTMLElement): number | undefined;
62
+ /**
63
+ * 全文档自动缩小:按 DOM 顺序处理所有 data-fit="shrink" 节点,
64
+ * 返回需要回写到模板的字号清单(key = 元素 id 或 `元素id#行类别#行:列`)。
65
+ * 调用方须在读取测量值之前执行,使实测高度与最终渲染同口径。
66
+ */
67
+ declare function applyTextFit(doc: Document): FitFontSize[];
68
+
69
+ /** 执行器版本:注入失败时用于日志定位产物不匹配 */
70
+ declare const EXECUTOR_VERSION = "2";
71
+
72
+ /** 等待文档加载、字体就绪与图片完成;任何失败都不阻断(测量有兜底) */
73
+ declare function waitReady(win: Window, timeoutMs?: number): Promise<void>;
74
+ /**
75
+ * 读取 [data-measure-id] 元素高度与表格行高(原始 CSS px)。
76
+ * 用 getBoundingClientRect().height 而非 offsetHeight:后者是取整整数,
77
+ * 38mm 会被舍成 144px(38.1mm),在内容刚好顶满分页预算时把「放得下」误判为
78
+ * 「放不下」,进而触发换页。亚像素精度才能反映真实布局高度。
79
+ */
80
+ declare function readMeasurements(doc: Document): RawMeasurement[];
81
+ /** 内容区后代相对 .print-page 顶部的最大底边(CSS px);绝对定位元素的几何由真实引擎决定 */
82
+ declare function readContentBottom(doc: Document): number;
83
+ /** 码值 → SVG 映射;单项失败跳过,交由 core 降级文本占位 */
84
+ declare function renderCodes(specs: CodeSpec[]): Record<string, string>;
85
+ /** 注入后挂在 globalThis.__wormDom 上的执行器对象 */
86
+ declare const domExecutor: {
87
+ version: string;
88
+ waitReady: typeof waitReady;
89
+ readMeasurements: typeof readMeasurements;
90
+ readContentBottom: typeof readContentBottom;
91
+ renderCodes: typeof renderCodes;
92
+ /** 自动缩小(data-fit="shrink"):须在 readMeasurements 之前调用,返回需回写的字号清单 */
93
+ applyTextFit: typeof applyTextFit;
94
+ };
95
+
96
+ export { type BrowserRenderOptions, type BrowserRenderResult, EXECUTOR_VERSION, applyTextFit, browserCodeRenderer, createBrowserPrintRuntime, createIframeDriverFactory, domExecutor, fitTextNode, readContentBottom, readMeasurements, renderCodes, renderHtmlPages, waitReady };
@@ -1,4 +1,5 @@
1
- import { P as PageLayout, T as TemplateData, C as CodeRenderer } from '../types-BGBAbQD5.js';
1
+ import { P as PageLayout, T as TemplateData, M as MultiPageTemplateData, C as CodeRenderer, D as DriverFactory, R as RawMeasurement, a as CodeSpec } from '../driver-Dn_YAzO5.js';
2
+ import { P as PrintRuntime, F as FitFontSize } from '../ports-BvwlA_km.js';
2
3
 
3
4
  interface BrowserRenderResult {
4
5
  /** 最终多页 HTML 字符串 */
@@ -7,17 +8,89 @@ interface BrowserRenderResult {
7
8
  pageCount: number;
8
9
  /** 分页布局(调试/高级用途) */
9
10
  pageLayouts: PageLayout[];
11
+ /** 最终纸张尺寸(mm,已含方向);连续纸为探针推导后的高度 */
12
+ paperMm: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /** 模板是否连续纸 */
17
+ continuous: boolean;
18
+ /** 份数(对象数据=1;数组=数组长度) */
19
+ copies: number;
20
+ /** 批量时每份纸张尺寸(连续纸各份高度可能不同) */
21
+ copyPaperMm?: {
22
+ width: number;
23
+ height: number;
24
+ }[];
25
+ }
26
+ interface BrowserRenderOptions {
27
+ /** 连续纸显式纸高覆盖(mm);仅对 CONTINUOUS 生效 */
28
+ paperHeightMm?: number;
29
+ /**
30
+ * 相对路径字体基址。缺省空串 = 不拼接,由文档自身 origin 解析——
31
+ * 与设计器画布注入 @font-face 的语义一致(画布能加载的字体,预览/浏览器打印也能加载)。
32
+ * 传值则拼在该基址上,供字体与页面不同源的宿主使用。
33
+ */
34
+ fontBaseUrl?: string;
10
35
  }
11
36
  /**
12
- * 浏览器内完成「数据绑定 → 测量 → 分页 → 最终 HTML」全流程。
13
- * @param template 模板 JSON(设计器 TemplateData 结构兼容)
37
+ * 浏览器内完成「数据绑定 → 码值渲染 → 测量 → 分页 → 连续纸推导 → 最终 HTML」。
38
+ * @param template 模板 JSON(设计器 TemplateData 结构兼容;含 { pages } 多页面模板)
14
39
  * @param printData 业务数据
15
40
  * @param baseUrl 图片相对路径拼接前缀
16
- * @param codeRenderer 条码渲染器(browserCodeRenderer)
41
+ * @param codeRenderer 调用方提供的码值渲染器;传入时沿用该渲染器(既有覆盖语义)
42
+ * @param options 渲染选项(连续纸纸高覆盖等)
17
43
  */
18
- declare function renderHtmlPages(template: TemplateData, printData?: Record<string, any> | Record<string, any>[], baseUrl?: string, codeRenderer?: CodeRenderer): Promise<BrowserRenderResult>;
44
+ declare function renderHtmlPages(template: TemplateData | MultiPageTemplateData, printData?: Record<string, any> | Record<string, any>[], baseUrl?: string, codeRenderer?: CodeRenderer, options?: BrowserRenderOptions): Promise<BrowserRenderResult>;
19
45
 
20
46
  /** 浏览器条码/二维码渲染器;码值非法或为空时抛错,由渲染管线降级文本占位 */
21
47
  declare const browserCodeRenderer: CodeRenderer;
22
48
 
23
- export { type BrowserRenderResult, browserCodeRenderer, renderHtmlPages };
49
+ /** 浏览器端 runtime:iframe driver + 进程内执行器;不提供 PDF/截图 */
50
+ declare function createBrowserPrintRuntime(): PrintRuntime;
51
+
52
+ /** 浏览器 iframe driver:进程内直调执行器,无需注入脚本;不提供出图能力 */
53
+ declare function createIframeDriverFactory(): DriverFactory;
54
+
55
+ /**
56
+ * 单个节点自动缩小;返回最终字号(pt)。非 shrink 节点返回 undefined。
57
+ * 缩到下限仍放不下时保持下限字号,由容器裁剪兜底(等价退化为截断)。
58
+ * 返回的字号与写进 DOM 的字号完全一致(向下取两位小数),
59
+ * 否则测量趟按 9.623pt 排版、最终趟按回写的 9.62pt 渲染,会出现不可控的亚像素错版。
60
+ */
61
+ declare function fitTextNode(el: HTMLElement): number | undefined;
62
+ /**
63
+ * 全文档自动缩小:按 DOM 顺序处理所有 data-fit="shrink" 节点,
64
+ * 返回需要回写到模板的字号清单(key = 元素 id 或 `元素id#行类别#行:列`)。
65
+ * 调用方须在读取测量值之前执行,使实测高度与最终渲染同口径。
66
+ */
67
+ declare function applyTextFit(doc: Document): FitFontSize[];
68
+
69
+ /** 执行器版本:注入失败时用于日志定位产物不匹配 */
70
+ declare const EXECUTOR_VERSION = "2";
71
+
72
+ /** 等待文档加载、字体就绪与图片完成;任何失败都不阻断(测量有兜底) */
73
+ declare function waitReady(win: Window, timeoutMs?: number): Promise<void>;
74
+ /**
75
+ * 读取 [data-measure-id] 元素高度与表格行高(原始 CSS px)。
76
+ * 用 getBoundingClientRect().height 而非 offsetHeight:后者是取整整数,
77
+ * 38mm 会被舍成 144px(38.1mm),在内容刚好顶满分页预算时把「放得下」误判为
78
+ * 「放不下」,进而触发换页。亚像素精度才能反映真实布局高度。
79
+ */
80
+ declare function readMeasurements(doc: Document): RawMeasurement[];
81
+ /** 内容区后代相对 .print-page 顶部的最大底边(CSS px);绝对定位元素的几何由真实引擎决定 */
82
+ declare function readContentBottom(doc: Document): number;
83
+ /** 码值 → SVG 映射;单项失败跳过,交由 core 降级文本占位 */
84
+ declare function renderCodes(specs: CodeSpec[]): Record<string, string>;
85
+ /** 注入后挂在 globalThis.__wormDom 上的执行器对象 */
86
+ declare const domExecutor: {
87
+ version: string;
88
+ waitReady: typeof waitReady;
89
+ readMeasurements: typeof readMeasurements;
90
+ readContentBottom: typeof readContentBottom;
91
+ renderCodes: typeof renderCodes;
92
+ /** 自动缩小(data-fit="shrink"):须在 readMeasurements 之前调用,返回需回写的字号清单 */
93
+ applyTextFit: typeof applyTextFit;
94
+ };
95
+
96
+ export { type BrowserRenderOptions, type BrowserRenderResult, EXECUTOR_VERSION, applyTextFit, browserCodeRenderer, createBrowserPrintRuntime, createIframeDriverFactory, domExecutor, fitTextNode, readContentBottom, readMeasurements, renderCodes, renderHtmlPages, waitReady };
@@ -1,141 +1,59 @@
1
1
  import {
2
- bindData,
3
- generateHtml,
4
- getPaperDimensions,
5
- paginate
6
- } from "../chunk-JZIVNXZ7.js";
7
-
8
- // src/browser/browser-pagination.ts
9
- var PX_PER_MM = 3.7795275591;
10
- var READY_TIMEOUT_MS = 3e3;
11
- async function renderHtmlPages(template, printData, baseUrl, codeRenderer) {
12
- const boundTemplate = bindData(template, printData, baseUrl);
13
- const measuredElements = await measureElements(boundTemplate, codeRenderer);
14
- const pageLayouts = paginate(boundTemplate, measuredElements);
15
- const html = generateHtml(boundTemplate, pageLayouts, printData, {
16
- codeRenderer
17
- });
18
- return { html, pageCount: pageLayouts.length, pageLayouts };
19
- }
20
- async function measureElements(template, codeRenderer) {
21
- const paper = getPaperDimensions(template);
22
- const measureHtml = generateHtml(template, [], void 0, {
23
- isMeasurementPass: true,
24
- codeRenderer
25
- });
26
- const iframe = document.createElement("iframe");
27
- iframe.setAttribute("aria-hidden", "true");
28
- iframe.style.cssText = `position:fixed;left:-10000px;top:0;width:${paper.width}mm;height:${paper.height}mm;border:0;visibility:hidden;pointer-events:none;`;
29
- document.body.appendChild(iframe);
30
- try {
31
- const win = iframe.contentWindow;
32
- const doc = iframe.contentDocument;
33
- if (!win || !doc) throw new Error("\u65E0\u6CD5\u521B\u5EFA\u6D4B\u91CF iframe");
34
- doc.open();
35
- doc.write(measureHtml);
36
- doc.close();
37
- await waitForRenderReady(win);
38
- const measurements = readMeasurements(doc);
39
- const measuredMap = /* @__PURE__ */ new Map();
40
- const elementIndex = new Map(template.elements.map((el) => [el.id, el]));
41
- for (const m of measurements) {
42
- const el = elementIndex.get(m.id);
43
- const repeatCount = el?.options?._repeatHeaderCount ?? 0;
44
- measuredMap.set(m.id, {
45
- id: m.id,
46
- measuredHeight: m.height,
47
- measuredRowHeights: m.rowHeights,
48
- repeatHeaderHeight: m.rowHeights && repeatCount > 0 ? m.rowHeights.slice(0, repeatCount).reduce((s, h) => s + h, 0) : 0
49
- });
50
- }
51
- return measuredMap;
52
- } finally {
53
- iframe.remove();
54
- }
55
- }
56
- function waitForRenderReady(win) {
57
- return new Promise((resolve) => {
58
- let settled = false;
59
- const finish = () => {
60
- if (!settled) {
61
- settled = true;
62
- resolve();
63
- }
64
- };
65
- const timer = setTimeout(finish, READY_TIMEOUT_MS);
66
- const run = async () => {
67
- try {
68
- const fonts = win.document.fonts;
69
- if (fonts?.ready) {
70
- await Promise.race([fonts.ready, timeout(READY_TIMEOUT_MS)]);
71
- }
72
- await Promise.race([waitForImages(win.document), timeout(READY_TIMEOUT_MS)]);
73
- } catch {
74
- } finally {
75
- clearTimeout(timer);
76
- finish();
77
- }
78
- };
79
- if (win.document.readyState === "complete") {
80
- void run();
81
- } else {
82
- win.addEventListener("load", () => void run(), { once: true });
83
- setTimeout(finish, READY_TIMEOUT_MS);
84
- }
85
- });
86
- }
87
- function timeout(ms) {
88
- return new Promise((_resolve, reject) => setTimeout(() => reject(new Error("timeout")), ms));
89
- }
90
- function waitForImages(doc) {
91
- const imgs = Array.from(doc.images ?? []);
92
- return Promise.all(
93
- imgs.map(
94
- (img) => img.complete ? Promise.resolve() : new Promise((resolve) => {
95
- img.addEventListener("load", () => resolve(), { once: true });
96
- img.addEventListener("error", () => resolve(), { once: true });
97
- })
98
- )
99
- ).then(() => void 0);
100
- }
101
- function readMeasurements(doc) {
102
- const result = [];
103
- const elements = doc.querySelectorAll("[data-measure-id]");
104
- for (const el of elements) {
105
- const htmlEl = el;
106
- const id = htmlEl.getAttribute("data-measure-id");
107
- if (!id) continue;
108
- const heightPx = htmlEl.offsetHeight;
109
- const table = htmlEl.querySelector("table.print-table");
110
- if (table) {
111
- const rowHeights = [];
112
- const rows = table.querySelectorAll("tbody > tr[data-row-index]");
113
- for (const row of rows) {
114
- rowHeights.push(row.offsetHeight / PX_PER_MM);
115
- }
116
- result.push({ id, height: heightPx / PX_PER_MM, rowHeights });
117
- } else {
118
- result.push({ id, height: heightPx / PX_PER_MM });
119
- }
120
- }
121
- return result;
122
- }
2
+ BARCODE_BAR_HEIGHT_MODULES,
3
+ BARCODE_MARGIN_BOTTOM_MODULES,
4
+ BARCODE_QUIET_ZONE_MODULES,
5
+ BARCODE_TEXT_FONT_SIZE_MODULES,
6
+ EXECUTOR_TARGETS,
7
+ barcodeUnitsPerModule,
8
+ createDomHostRuntime,
9
+ floorFontSize,
10
+ mmToPx,
11
+ prepareDocument,
12
+ resolveBarcodeSize,
13
+ resolveShrinkMinFontSize
14
+ } from "../chunk-KFRPTLFV.js";
123
15
 
124
16
  // src/browser/browser-code-renderer.ts
125
17
  import JsBarcode from "jsbarcode";
126
18
  import QRCode from "qrcode";
127
19
  var SVG_NS = "http://www.w3.org/2000/svg";
20
+ function readViewBox(svg) {
21
+ const raw = svg.getAttribute("viewBox");
22
+ if (!raw) return null;
23
+ const parts = raw.trim().split(/[\s,]+/).map(Number);
24
+ if (parts.length !== 4 || parts.some((v) => !Number.isFinite(v))) return null;
25
+ return { width: parts[2], height: parts[3] };
26
+ }
128
27
  function renderBarcodeSvg(value, opts) {
129
28
  const svg = document.createElementNS(SVG_NS, "svg");
29
+ const unitPerModule = barcodeUnitsPerModule(opts.barWidth);
130
30
  JsBarcode(svg, value, {
131
31
  format: opts.barcodeType || "CODE128",
132
- width: Math.max(1, (opts.barWidth ?? 2) / 2),
133
- height: 30,
32
+ width: unitPerModule,
33
+ height: BARCODE_BAR_HEIGHT_MODULES * unitPerModule,
134
34
  displayValue: opts.showText !== false,
135
- fontSize: opts.fontSize ?? 10,
35
+ fontSize: (opts.fontSize ?? BARCODE_TEXT_FONT_SIZE_MODULES) * unitPerModule,
36
+ // 静区只留左右:上下留白会白白吃掉元素高度(jsbarcode 的 margin 是四边通配)
136
37
  margin: 0,
137
- marginBottom: 2
38
+ marginLeft: BARCODE_QUIET_ZONE_MODULES * unitPerModule,
39
+ marginRight: BARCODE_QUIET_ZONE_MODULES * unitPerModule,
40
+ marginTop: 0,
41
+ marginBottom: BARCODE_MARGIN_BOTTOM_MODULES * unitPerModule
138
42
  });
43
+ const viewBox = readViewBox(svg);
44
+ if (viewBox) {
45
+ const size = resolveBarcodeSize({
46
+ unitWidth: viewBox.width / unitPerModule,
47
+ unitHeight: viewBox.height / unitPerModule,
48
+ boxWidthMm: opts.targetWidthMm ?? 0,
49
+ boxHeightMm: opts.targetHeightMm ?? 0,
50
+ dpi: opts.printerDpi,
51
+ barWidth: opts.barWidth
52
+ });
53
+ svg.setAttribute("width", `${size.widthMm}mm`);
54
+ svg.setAttribute("height", `${size.heightMm}mm`);
55
+ }
56
+ svg.setAttribute("shape-rendering", "crispEdges");
139
57
  return svg.outerHTML;
140
58
  }
141
59
  function renderQrSvg(value, opts) {
@@ -158,13 +76,250 @@ function renderQrSvg(value, opts) {
158
76
  }
159
77
  return `<svg xmlns="${SVG_NS}" viewBox="0 0 ${dim} ${dim}" width="${dim}" height="${dim}" shape-rendering="crispEdges">${rects}</svg>`;
160
78
  }
79
+ function renderCodeSvg(value, cellType, opts = {}) {
80
+ if (!value) throw new Error("empty barcode value");
81
+ return cellType === "qrcode" ? renderQrSvg(value, opts) : renderBarcodeSvg(value, opts);
82
+ }
161
83
  var browserCodeRenderer = {
162
84
  render(value, cellType, opts = {}) {
163
- if (!value) throw new Error("empty barcode value");
164
- return cellType === "qrcode" ? renderQrSvg(value, opts) : renderBarcodeSvg(value, opts);
85
+ return renderCodeSvg(value, cellType, opts);
86
+ }
87
+ };
88
+
89
+ // src/browser/text-fit-dom.ts
90
+ var FIT_TOLERANCE_PX = 0.5;
91
+ var SEARCH_STEPS = 12;
92
+ function readAttrNumber(el, name) {
93
+ const raw = el.getAttribute(name);
94
+ if (raw === null || raw === "") return void 0;
95
+ const value = Number(raw);
96
+ return Number.isFinite(value) ? value : void 0;
97
+ }
98
+ function overflows(el, targetPx) {
99
+ if (el.scrollWidth > el.clientWidth + FIT_TOLERANCE_PX) return true;
100
+ const content = el.scrollHeight;
101
+ if (targetPx !== void 0) return content > targetPx + FIT_TOLERANCE_PX;
102
+ return content > el.clientHeight + FIT_TOLERANCE_PX;
103
+ }
104
+ function fitTextNode(el) {
105
+ if (el.getAttribute("data-fit") !== "shrink") return void 0;
106
+ const minPt = resolveShrinkMinFontSize(readAttrNumber(el, "data-fit-min"));
107
+ const start = Math.max(readAttrNumber(el, "data-fit-base") ?? minPt, minPt);
108
+ const targetMm = readAttrNumber(el, "data-fit-mm");
109
+ const targetPx = targetMm !== void 0 && targetMm > 0 ? mmToPx(targetMm) : void 0;
110
+ const apply = (pt) => {
111
+ const size = floorFontSize(pt);
112
+ el.style.fontSize = `${size}pt`;
113
+ el.setAttribute("data-fit-size", String(size));
114
+ return size;
115
+ };
116
+ const base = apply(start);
117
+ if (!overflows(el, targetPx)) return base;
118
+ if (start <= minPt) return base;
119
+ let lo = minPt;
120
+ let hi = start;
121
+ for (let i = 0; i < SEARCH_STEPS; i++) {
122
+ const mid = (lo + hi) / 2;
123
+ apply(mid);
124
+ if (overflows(el, targetPx)) hi = mid;
125
+ else lo = mid;
165
126
  }
127
+ return apply(lo);
128
+ }
129
+ function applyTextFit(doc) {
130
+ const fits = [];
131
+ doc.querySelectorAll('[data-fit="shrink"]').forEach((el) => {
132
+ const size = fitTextNode(el);
133
+ const key = el.getAttribute("data-fit-key");
134
+ if (size === void 0 || !key) return;
135
+ fits.push({ key, fontSizePt: size });
136
+ });
137
+ return fits;
138
+ }
139
+
140
+ // src/browser/dom-executor.ts
141
+ var EXECUTOR_VERSION = "2";
142
+ var DEFAULT_READY_TIMEOUT_MS = 5e3;
143
+ async function waitReady(win, timeoutMs = DEFAULT_READY_TIMEOUT_MS) {
144
+ const doc = win.document;
145
+ const wait = (task) => Promise.race([task, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
146
+ const ready = (async () => {
147
+ if (doc.readyState !== "complete") {
148
+ await wait(new Promise((resolve) => win.addEventListener("load", () => resolve(), { once: true })));
149
+ }
150
+ const fonts = doc.fonts;
151
+ if (fonts) {
152
+ await wait(Promise.all(declaredFontFaces(doc).map(
153
+ (face) => fonts.load(`${face.style} ${face.weight} 16px "${face.family}"`).catch(() => void 0)
154
+ )));
155
+ if (fonts.ready) await wait(fonts.ready);
156
+ }
157
+ await wait(Promise.all(Array.from(doc.images ?? []).map(
158
+ (img) => img.complete ? Promise.resolve() : new Promise((resolve) => {
159
+ img.addEventListener("load", () => resolve(), { once: true });
160
+ img.addEventListener("error", () => resolve(), { once: true });
161
+ })
162
+ )));
163
+ })();
164
+ await wait(ready);
165
+ }
166
+ function declaredFontFaces(doc) {
167
+ const out = [];
168
+ const CSS_FONT_FACE_RULE = 5;
169
+ for (const sheet of Array.from(doc.styleSheets ?? [])) {
170
+ let rules;
171
+ try {
172
+ rules = sheet.cssRules;
173
+ } catch {
174
+ continue;
175
+ }
176
+ for (const rule of Array.from(rules ?? [])) {
177
+ if (rule.type !== CSS_FONT_FACE_RULE) continue;
178
+ const style = rule.style;
179
+ const family = (style.getPropertyValue("font-family") || "").replace(/^["']|["']$/g, "");
180
+ if (!family) continue;
181
+ out.push({
182
+ family,
183
+ weight: style.getPropertyValue("font-weight") || "400",
184
+ style: style.getPropertyValue("font-style") || "normal"
185
+ });
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ function readMeasurements(doc) {
191
+ const result = [];
192
+ doc.querySelectorAll("[data-measure-id]").forEach((node) => {
193
+ const el = node;
194
+ const id = el.getAttribute("data-measure-id");
195
+ if (!id) return;
196
+ const heightPx = el.getBoundingClientRect().height;
197
+ const table = el.querySelector("table.print-table");
198
+ if (!table) {
199
+ result.push({ id, heightPx });
200
+ return;
201
+ }
202
+ const rowHeightsPx = [];
203
+ table.querySelectorAll("tbody > tr[data-row-index]").forEach((row) => {
204
+ rowHeightsPx.push(row.getBoundingClientRect().height);
205
+ });
206
+ result.push({ id, heightPx, rowHeightsPx });
207
+ });
208
+ return result;
209
+ }
210
+ function readContentBottom(doc) {
211
+ const page = doc.querySelector(".print-page");
212
+ const area = doc.querySelector(".content-area");
213
+ if (!page || !area) return 0;
214
+ const pageTop = page.getBoundingClientRect().top;
215
+ let maxBottom = 0;
216
+ area.querySelectorAll("*").forEach((node) => {
217
+ const rect = node.getBoundingClientRect();
218
+ if (rect.height > 0) maxBottom = Math.max(maxBottom, rect.bottom - pageTop);
219
+ });
220
+ const areaRect = area.getBoundingClientRect();
221
+ if (areaRect.height > 0) maxBottom = Math.max(maxBottom, areaRect.bottom - pageTop);
222
+ return maxBottom;
223
+ }
224
+ function renderCodes(specs) {
225
+ const map = {};
226
+ for (const spec of specs) {
227
+ try {
228
+ map[spec.key] = renderCodeSvg(spec.value, spec.cellType, spec.opts);
229
+ } catch {
230
+ }
231
+ }
232
+ return map;
233
+ }
234
+ var domExecutor = {
235
+ version: EXECUTOR_VERSION,
236
+ waitReady,
237
+ readMeasurements,
238
+ readContentBottom,
239
+ renderCodes,
240
+ /** 自动缩小(data-fit="shrink"):须在 readMeasurements 之前调用,返回需回写的字号清单 */
241
+ applyTextFit
166
242
  };
243
+
244
+ // src/browser/driver-iframe.ts
245
+ function createIframeDriverFactory() {
246
+ return {
247
+ async createDriver() {
248
+ const iframe = document.createElement("iframe");
249
+ iframe.setAttribute("aria-hidden", "true");
250
+ iframe.style.cssText = "position:fixed;left:-10000px;top:0;border:0;visibility:hidden;pointer-events:none;";
251
+ document.body.appendChild(iframe);
252
+ let doc = iframe.contentDocument ?? document;
253
+ return {
254
+ requiresExecutor: false,
255
+ async open(viewport) {
256
+ iframe.style.width = `${viewport.width}px`;
257
+ iframe.style.height = `${viewport.height}px`;
258
+ doc = iframe.contentDocument ?? document;
259
+ },
260
+ async setContent(html) {
261
+ doc = iframe.contentDocument ?? document;
262
+ doc.open();
263
+ doc.write(html);
264
+ doc.close();
265
+ },
266
+ async injectExecutor(_bundle) {
267
+ },
268
+ async evaluate(method, args) {
269
+ const fn = domExecutor[method];
270
+ const payload = args ?? [];
271
+ const target = EXECUTOR_TARGETS[method];
272
+ if (target === "window") return fn(iframe.contentWindow, ...payload);
273
+ if (target === "document") return fn(doc, ...payload);
274
+ return fn(...payload);
275
+ },
276
+ async close() {
277
+ iframe.remove();
278
+ }
279
+ };
280
+ }
281
+ };
282
+ }
283
+
284
+ // src/browser/browser-runtime.ts
285
+ function createBrowserPrintRuntime() {
286
+ return createDomHostRuntime(createIframeDriverFactory());
287
+ }
288
+
289
+ // src/browser/browser-pagination.ts
290
+ async function renderHtmlPages(template, printData, baseUrl, codeRenderer, options) {
291
+ const prepared = await prepareDocument(
292
+ {
293
+ templateJson: template,
294
+ printData,
295
+ baseUrl,
296
+ fontBaseUrl: options?.fontBaseUrl ?? "",
297
+ paperHeightMm: options?.paperHeightMm,
298
+ codeRenderer
299
+ },
300
+ createBrowserPrintRuntime()
301
+ );
302
+ return {
303
+ html: prepared.html,
304
+ pageCount: prepared.pageCount,
305
+ pageLayouts: prepared.pageLayouts,
306
+ paperMm: prepared.paperMm,
307
+ continuous: prepared.continuous,
308
+ copies: prepared.copies,
309
+ copyPaperMm: prepared.copyPaperMm
310
+ };
311
+ }
167
312
  export {
313
+ EXECUTOR_VERSION,
314
+ applyTextFit,
168
315
  browserCodeRenderer,
169
- renderHtmlPages
316
+ createBrowserPrintRuntime,
317
+ createIframeDriverFactory,
318
+ domExecutor,
319
+ fitTextNode,
320
+ readContentBottom,
321
+ readMeasurements,
322
+ renderCodes,
323
+ renderHtmlPages,
324
+ waitReady
170
325
  };
@@ -3,7 +3,7 @@ import {
3
3
  evaluate,
4
4
  parse,
5
5
  tokenize
6
- } from "./chunk-JZIVNXZ7.js";
6
+ } from "./chunk-KFRPTLFV.js";
7
7
 
8
8
  // src/index.ts
9
9
  var TemplateEngine = class {