mixdog 1.0.4 → 1.0.6
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 +117 -434
- package/package.json +18 -17
- package/scripts/gate-local.mjs +93 -0
- package/scripts/lib/gate-scheduler.mjs +24 -0
- package/scripts/lib/run-node-tests.mjs +5 -1
- package/scripts/release-paths.mjs +22 -1
- package/scripts/test-direct.mjs +2 -1
- package/scripts/test.mjs +3 -1
- package/src/defaults/agents.json +0 -12
- package/src/defaults/mixdog-config.template.json +1 -2
- package/src/defaults/skills/docx/SKILL.md +7 -4
- package/src/defaults/skills/docx/references/html.md +46 -0
- package/src/defaults/skills/docx/references/native-authoring.md +18 -4
- package/src/defaults/skills/pdf/SKILL.md +10 -2
- package/src/defaults/skills/pdf/references/html.md +64 -0
- package/src/defaults/skills/pptx/references/html.md +1 -1
- package/src/defaults/skills/xlsx/SKILL.md +7 -3
- package/src/defaults/skills/xlsx/references/html.md +40 -0
- package/src/defaults/skills/xlsx/references/report-design.md +21 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
- package/src/runtime/attachments/pdf-extract.mjs +7 -3
- package/src/runtime/computer-bridge/client.mjs +1 -1
- package/src/runtime/memory/index.mjs +4 -15
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
- package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
- package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
- package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
- package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
- package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
- package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
- package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
- package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
- package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
- package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
- package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
- package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
- package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
- package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
- package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
- package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-embed.mjs +14 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
- package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +2 -6
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +1 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
- package/src/runtime/memory/lib/recall-format.mjs +1 -1
- package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
- package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
- package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
- package/src/runtime/office/authoring/html-browser.mjs +61 -0
- package/src/runtime/office/authoring/html-document-author.mjs +181 -0
- package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
- package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
- package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
- package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
- package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
- package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
- package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
- package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
- package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
- package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
- package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
- package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
- package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
- package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
- package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
- package/src/runtime/office/capabilities-catalog.mjs +8 -0
- package/src/runtime/office/capabilities-signatures.mjs +18 -6
- package/src/runtime/office/capabilities.mjs +26 -1
- package/src/runtime/office/com/office-com-host.ps1 +133 -17
- package/src/runtime/office/core/office-actions-batch.mjs +16 -1
- package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
- package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
- package/src/runtime/office/core/office-actions-render.mjs +4 -1
- package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
- package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
- package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
- package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
- package/src/runtime/office/core/office-sessions.mjs +19 -2
- package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
- package/src/runtime/office/portable/docx-formatting.mjs +21 -0
- package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
- package/src/runtime/office/portable/portable-chart.mjs +10 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
- package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
- package/src/runtime/office/portable/portable-docx.mjs +2 -0
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
- package/src/runtime/office/portable/portable-validation.mjs +6 -3
- package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
- package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
- package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
- package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
- package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
- package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
- package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
- package/src/runtime/office/quality/design-review-critique.mjs +3 -2
- package/src/runtime/office/quality/document-brief.mjs +44 -0
- package/src/runtime/office/quality/inline-audit.mjs +7 -2
- package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
- package/src/runtime/office/tool-defs.mjs +2 -2
- package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
- package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
- package/src/runtime/shared/provider-accounts.mjs +11 -1
- package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/wait-until.test-support.mjs +33 -0
- package/src/session-runtime/boot/apis.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +4 -3
- package/src/session-runtime/services/usage-stats-model.mjs +22 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
- package/src/session-runtime/turn/session-ops.mjs +15 -9
- package/src/session-runtime/usage-stats-api.mjs +34 -2
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/local-session-runtime.mjs +488 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host.mjs +1 -1
- package/src/tui/hooks/useSession.mjs +1 -1
- package/src/tui/session/session-api/commands.mjs +1 -1
- package/src/tui/session/session-api/integrations.mjs +3 -0
- package/src/tui/session/session-flow/auto-clear.mjs +6 -7
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/turn.mjs +5 -0
- package/src/tui/session-local.mjs +3 -488
- package/src/tui/session.mjs +1 -1
- package/src/ui/statusline-agents.mjs +0 -1
- package/src/rules/agent/41-cycle2-agent.md +0 -24
- package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
- package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
- package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
- package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
// The Word HTML route, first half: the browser lays the document out at the printable width of its @page sheet and
|
|
2
|
+
// this page function reads it back as Word's own flow — paragraphs with their runs, tables (data tables, and the flex
|
|
3
|
+
// or grid rows a layout draws, as borderless tables), boxes (a filled or bordered block, as a one-cell table), lists,
|
|
4
|
+
// pictures, and the vertical space between blocks as the browser measured it. docx-html-build.mjs writes the flow as
|
|
5
|
+
// native WordprocessingML. The approach follows dom-docx (computed styles from Chromium, flex rows as borderless
|
|
6
|
+
// tables, boxes as one-cell tables); CSS that has no place in Word's flow (position, transform) is reported, not drawn.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs inside the page (page.evaluate): self-contained. Reads document.body as Word blocks.
|
|
10
|
+
* @returns {{ blocks: object[], header: object|null, footer: object|null, body: object, notes: string[], captures: number }}
|
|
11
|
+
*/
|
|
12
|
+
export function extractDocxFlow() {
|
|
13
|
+
const PT = 0.75;
|
|
14
|
+
const pt = (px) => Math.round(px * PT * 100) / 100;
|
|
15
|
+
const n = (value) => parseFloat(value) || 0;
|
|
16
|
+
const notes = [];
|
|
17
|
+
let captures = 0;
|
|
18
|
+
let lists = 0;
|
|
19
|
+
const styles = new Map();
|
|
20
|
+
const gcs = (el) => {
|
|
21
|
+
if (!styles.has(el)) styles.set(el, getComputedStyle(el));
|
|
22
|
+
return styles.get(el);
|
|
23
|
+
};
|
|
24
|
+
// A colour as Word writes it: six hex digits, a translucent one laid over white; null for none.
|
|
25
|
+
const hex = (value) => {
|
|
26
|
+
const match = /rgba?\(([^)]+)\)/.exec(value || '');
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const parts = match[1]
|
|
29
|
+
.split(/[\s,/]+/)
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map((part) => (part.endsWith('%') ? (parseFloat(part) / 100) * 255 : parseFloat(part)));
|
|
32
|
+
const alpha = parts.length > 3 ? (match[1].includes('/') || parts[3] <= 1 ? parts[3] : parts[3] / 255) : 1;
|
|
33
|
+
if (!(alpha > 0.04)) return null;
|
|
34
|
+
return [0, 1, 2]
|
|
35
|
+
.map((index) =>
|
|
36
|
+
Math.round(parts[index] * alpha + 255 * (1 - alpha))
|
|
37
|
+
.toString(16)
|
|
38
|
+
.padStart(2, '0')
|
|
39
|
+
)
|
|
40
|
+
.join('')
|
|
41
|
+
.toUpperCase();
|
|
42
|
+
};
|
|
43
|
+
const GENERIC = new Set([
|
|
44
|
+
'serif',
|
|
45
|
+
'sans-serif',
|
|
46
|
+
'monospace',
|
|
47
|
+
'cursive',
|
|
48
|
+
'fantasy',
|
|
49
|
+
'system-ui',
|
|
50
|
+
'ui-sans-serif',
|
|
51
|
+
'ui-serif',
|
|
52
|
+
]);
|
|
53
|
+
const face = (cs) => {
|
|
54
|
+
const first = String(cs.fontFamily || '')
|
|
55
|
+
.split(',')[0]
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/^["']|["']$/g, '');
|
|
58
|
+
return GENERIC.has(first.toLowerCase()) ? '' : first;
|
|
59
|
+
};
|
|
60
|
+
const describe = (el) => {
|
|
61
|
+
const classes =
|
|
62
|
+
typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/).join('.')}` : '';
|
|
63
|
+
const text = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 30);
|
|
64
|
+
return `<${el.localName}${classes}>${text ? ` "${text}"` : ''}`;
|
|
65
|
+
};
|
|
66
|
+
const visible = (el) => {
|
|
67
|
+
const cs = gcs(el);
|
|
68
|
+
return cs.display !== 'none' && cs.visibility !== 'hidden';
|
|
69
|
+
};
|
|
70
|
+
const BLOCK = new Set(['block', 'flex', 'grid', 'table', 'list-item', 'flow-root', 'table-row-group', 'table-row']);
|
|
71
|
+
const blockChildren = (el) => [...el.children].filter((child) => visible(child) && BLOCK.has(gcs(child).display));
|
|
72
|
+
const contentBox = (el) => {
|
|
73
|
+
const cs = gcs(el);
|
|
74
|
+
const r = el.getBoundingClientRect();
|
|
75
|
+
return {
|
|
76
|
+
left: r.left + n(cs.borderLeftWidth) + n(cs.paddingLeft),
|
|
77
|
+
right: r.right - n(cs.borderRightWidth) - n(cs.paddingRight),
|
|
78
|
+
top: r.top + n(cs.borderTopWidth) + n(cs.paddingTop),
|
|
79
|
+
bottom: r.bottom - n(cs.borderBottomWidth) - n(cs.paddingBottom),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const side = (cs, name) => {
|
|
83
|
+
const width = n(cs[`border${name}Width`]);
|
|
84
|
+
const style = cs[`border${name}Style`];
|
|
85
|
+
const color = hex(cs[`border${name}Color`]);
|
|
86
|
+
if (!(width > 0) || style === 'none' || style === 'hidden' || !color) return null;
|
|
87
|
+
return { width: pt(width), color, style: ['dashed', 'dotted', 'double'].includes(style) ? style : 'single' };
|
|
88
|
+
};
|
|
89
|
+
const borders = (cs) => {
|
|
90
|
+
const sides = {
|
|
91
|
+
top: side(cs, 'Top'),
|
|
92
|
+
left: side(cs, 'Left'),
|
|
93
|
+
bottom: side(cs, 'Bottom'),
|
|
94
|
+
right: side(cs, 'Right'),
|
|
95
|
+
};
|
|
96
|
+
return Object.values(sides).some(Boolean) ? sides : null;
|
|
97
|
+
};
|
|
98
|
+
const decorated = (cs) => Boolean(hex(cs.backgroundColor)) || Boolean(borders(cs));
|
|
99
|
+
const checkUnsupported = (el, cs) => {
|
|
100
|
+
if (['absolute', 'fixed'].includes(cs.position)) {
|
|
101
|
+
notes.push(`${describe(el)}: position ${cs.position} has no place in Word's flow; it was left out`);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (cs.transform && cs.transform !== 'none')
|
|
105
|
+
notes.push(`${describe(el)}: transform is not carried to Word; drawn upright`);
|
|
106
|
+
if (n(cs.columnCount) > 1) notes.push(`${describe(el)}: CSS columns are not carried; the text runs in one column`);
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// The runs of a block's inline content, in its own type.
|
|
111
|
+
const runStyle = (el) => {
|
|
112
|
+
const cs = gcs(el);
|
|
113
|
+
const weight = Number(cs.fontWeight) || (cs.fontWeight === 'bold' ? 700 : 400);
|
|
114
|
+
const decoration = String(cs.textDecorationLine || cs.textDecoration || '');
|
|
115
|
+
const inlineFill = ['inline', 'inline-block'].includes(cs.display) ? hex(cs.backgroundColor) : null;
|
|
116
|
+
return {
|
|
117
|
+
font: face(cs),
|
|
118
|
+
size: pt(n(cs.fontSize)),
|
|
119
|
+
bold: weight >= 600,
|
|
120
|
+
italic: cs.fontStyle === 'italic' || cs.fontStyle === 'oblique',
|
|
121
|
+
underline: decoration.includes('underline'),
|
|
122
|
+
strike: decoration.includes('line-through'),
|
|
123
|
+
color: hex(cs.color) || '000000',
|
|
124
|
+
...(inlineFill ? { shading: inlineFill } : {}),
|
|
125
|
+
...(['super', 'sub'].includes(cs.verticalAlign)
|
|
126
|
+
? { vertAlign: cs.verticalAlign === 'super' ? 'superscript' : 'subscript' }
|
|
127
|
+
: {}),
|
|
128
|
+
transform: cs.textTransform,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const collectRuns = (node, runs, skip = () => false) => {
|
|
132
|
+
for (const child of node.childNodes) {
|
|
133
|
+
if (child.nodeType === 3) {
|
|
134
|
+
const text = child.textContent.replace(/[\t\n\r ]+/g, ' ');
|
|
135
|
+
if (!text) continue;
|
|
136
|
+
const style = runStyle(child.parentElement);
|
|
137
|
+
let value = text;
|
|
138
|
+
if (style.transform === 'uppercase') value = value.toUpperCase();
|
|
139
|
+
else if (style.transform === 'lowercase') value = value.toLowerCase();
|
|
140
|
+
const { transform: _transform, ...format } = style;
|
|
141
|
+
runs.push({ text: value, ...format });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (child.nodeType !== 1 || !visible(child) || skip(child)) continue;
|
|
145
|
+
const tag = child.localName;
|
|
146
|
+
if (tag === 'br') {
|
|
147
|
+
runs.push({ br: true });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (['img', 'svg', 'canvas'].includes(tag)) {
|
|
151
|
+
notes.push(`${describe(child)}: a picture inside a line of text is left out; set it as a block of its own`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
collectRuns(child, runs, skip);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
// Whitespace as the browser collapses it: one space between words, none at a line's ends.
|
|
158
|
+
const tidyRuns = (runs) => {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const run of runs) {
|
|
161
|
+
if (run.br) {
|
|
162
|
+
if (out.length && out.at(-1).text) out.at(-1).text = out.at(-1).text.replace(/ $/, '');
|
|
163
|
+
out.push(run);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
let text = run.text;
|
|
167
|
+
const previous = out.at(-1);
|
|
168
|
+
if (!previous || previous.br || / $/.test(previous.text || '')) text = text.replace(/^ /, '');
|
|
169
|
+
if (text) out.push({ ...run, text });
|
|
170
|
+
}
|
|
171
|
+
while (out.length && out[0].br) out.shift();
|
|
172
|
+
if (out.length && out.at(-1).text) out.at(-1).text = out.at(-1).text.replace(/ $/, '');
|
|
173
|
+
return out.filter((run) => run.br || run.text);
|
|
174
|
+
};
|
|
175
|
+
const alignOf = (cs) => {
|
|
176
|
+
const value = String(cs.textAlign || '');
|
|
177
|
+
if (value === 'center') return 'center';
|
|
178
|
+
if (value === 'right' || value === 'end') return 'right';
|
|
179
|
+
if (value === 'justify') return 'both';
|
|
180
|
+
return 'left';
|
|
181
|
+
};
|
|
182
|
+
const lineOf = (cs) => (cs.lineHeight === 'normal' ? pt(n(cs.fontSize) * 1.3) : pt(n(cs.lineHeight)));
|
|
183
|
+
|
|
184
|
+
const paragraphOf = (el, frame, extra = {}) => {
|
|
185
|
+
const cs = gcs(el);
|
|
186
|
+
const runs = tidyRuns(
|
|
187
|
+
(() => {
|
|
188
|
+
const list = [];
|
|
189
|
+
collectRuns(el, list, extra.skip);
|
|
190
|
+
return list;
|
|
191
|
+
})()
|
|
192
|
+
);
|
|
193
|
+
const box = contentBox(el);
|
|
194
|
+
const tag = el.localName;
|
|
195
|
+
const level = /^h([1-6])$/.exec(tag)?.[1];
|
|
196
|
+
const ownBorders = extra.noDecoration ? null : borders(cs);
|
|
197
|
+
const fill = extra.noDecoration ? null : hex(cs.backgroundColor);
|
|
198
|
+
return {
|
|
199
|
+
kind: 'p',
|
|
200
|
+
tag,
|
|
201
|
+
...(level ? { heading: Number(level) } : {}),
|
|
202
|
+
runs,
|
|
203
|
+
align: alignOf(cs),
|
|
204
|
+
line: lineOf(cs),
|
|
205
|
+
size: pt(n(cs.fontSize)),
|
|
206
|
+
indentLeft: Math.max(0, pt(box.left - frame.left)),
|
|
207
|
+
indentRight: Math.max(0, pt(frame.right - box.right)),
|
|
208
|
+
firstLine: pt(n(cs.textIndent)),
|
|
209
|
+
keepNext: Boolean(level) || cs.breakAfter === 'avoid' || cs.pageBreakAfter === 'avoid',
|
|
210
|
+
keepLines: cs.breakInside === 'avoid' || cs.pageBreakInside === 'avoid',
|
|
211
|
+
...(fill ? { shading: fill } : {}),
|
|
212
|
+
...(ownBorders
|
|
213
|
+
? {
|
|
214
|
+
borders: ownBorders,
|
|
215
|
+
borderSpace: {
|
|
216
|
+
top: pt(n(cs.paddingTop)),
|
|
217
|
+
left: pt(n(cs.paddingLeft)),
|
|
218
|
+
bottom: pt(n(cs.paddingBottom)),
|
|
219
|
+
right: pt(n(cs.paddingRight)),
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
: {}),
|
|
223
|
+
// Word sets a top or bottom rule and its gap (the padding) on the paragraph itself, so the space around it is
|
|
224
|
+
// measured from the rule's outer edge: from the text, the rule's gap and width were counted twice.
|
|
225
|
+
top: extra.top ?? (ownBorders?.top ? el.getBoundingClientRect().top : box.top),
|
|
226
|
+
bottom: extra.bottom ?? (ownBorders?.bottom ? el.getBoundingClientRect().bottom : box.bottom),
|
|
227
|
+
...(extra.list ? { list: extra.list } : {}),
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// A grid of cells from their boxes: the columns are the distinct vertical edges, so a flex row of cards, a CSS grid,
|
|
232
|
+
// and a data table with colspans all become one Word grid; a gap between cells is a column of its own, left empty.
|
|
233
|
+
const gridTable = (cells, frame, { data = false } = {}) => {
|
|
234
|
+
const rows = [];
|
|
235
|
+
for (const cell of [...cells].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left)) {
|
|
236
|
+
const row = rows.find(
|
|
237
|
+
(entry) => Math.abs(entry.top - cell.rect.top) <= 2 && (data ? entry.tr === cell.tr : true)
|
|
238
|
+
);
|
|
239
|
+
if (row) {
|
|
240
|
+
row.cells.push(cell);
|
|
241
|
+
row.bottom = Math.max(row.bottom, cell.rect.bottom);
|
|
242
|
+
} else
|
|
243
|
+
rows.push({ top: cell.rect.top, bottom: cell.rect.bottom, cells: [cell], tr: cell.tr, header: cell.header });
|
|
244
|
+
}
|
|
245
|
+
const edges = [];
|
|
246
|
+
const addEdge = (x) => {
|
|
247
|
+
if (!edges.some((edge) => Math.abs(edge - x) <= 1)) edges.push(x);
|
|
248
|
+
};
|
|
249
|
+
for (const cell of cells) {
|
|
250
|
+
addEdge(cell.rect.left);
|
|
251
|
+
addEdge(cell.rect.right);
|
|
252
|
+
}
|
|
253
|
+
edges.sort((a, b) => a - b);
|
|
254
|
+
const edgeIndex = (x) => edges.findIndex((edge) => Math.abs(edge - x) <= 1);
|
|
255
|
+
const columns = edges.slice(1).map((edge, index) => pt(edge - edges[index]));
|
|
256
|
+
const outRows = [];
|
|
257
|
+
rows.forEach((row, rowIndex) => {
|
|
258
|
+
if (rowIndex > 0) {
|
|
259
|
+
const gap = row.top - rows[rowIndex - 1].bottom;
|
|
260
|
+
if (gap > 1) outRows.push({ spacer: true, height: pt(gap) });
|
|
261
|
+
}
|
|
262
|
+
const placed = [];
|
|
263
|
+
let at = 0;
|
|
264
|
+
for (const cell of row.cells.sort((a, b) => a.rect.left - b.rect.left)) {
|
|
265
|
+
const start = edgeIndex(cell.rect.left);
|
|
266
|
+
const end = edgeIndex(cell.rect.right);
|
|
267
|
+
if (start > at) placed.push({ span: start - at, empty: true });
|
|
268
|
+
placed.push({ ...cell.content, span: Math.max(1, end - start) });
|
|
269
|
+
at = Math.max(at, end);
|
|
270
|
+
}
|
|
271
|
+
if (at < columns.length) placed.push({ span: columns.length - at, empty: true });
|
|
272
|
+
outRows.push({ height: pt(row.bottom - row.top), header: Boolean(row.header), cells: placed });
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
kind: 'table',
|
|
276
|
+
data,
|
|
277
|
+
indent: Math.max(0, pt(edges[0] - frame.left)),
|
|
278
|
+
columns,
|
|
279
|
+
rows: outRows,
|
|
280
|
+
top: rows[0]?.top ?? 0,
|
|
281
|
+
bottom: rows.at(-1)?.bottom ?? 0,
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// A cell's own decoration and its content, read as a flow inside its padding.
|
|
286
|
+
// `given` blocks stand in for the element's own flow (a decorated row's grid, already read).
|
|
287
|
+
const cellOf = (el, { tr = null, header = false, given = null } = {}) => {
|
|
288
|
+
const cs = gcs(el);
|
|
289
|
+
const rect = el.getBoundingClientRect();
|
|
290
|
+
const inner = contentBox(el);
|
|
291
|
+
const fill = hex(cs.backgroundColor) || (tr ? hex(gcs(tr).backgroundColor) : null);
|
|
292
|
+
const blocks = given ? [...given] : [];
|
|
293
|
+
if (!given && blockChildren(el).length) flow(el, inner, blocks);
|
|
294
|
+
else if (!given) blocks.push(paragraphOf(el, inner, { noDecoration: true }));
|
|
295
|
+
spaceBlocks(blocks, inner.top);
|
|
296
|
+
const vertical = cs.verticalAlign === 'middle' ? 'center' : cs.verticalAlign === 'bottom' ? 'bottom' : 'top';
|
|
297
|
+
const centred = ['center'].includes(cs.alignItems) && /flex|grid/.test(cs.display) ? 'center' : vertical;
|
|
298
|
+
return {
|
|
299
|
+
rect,
|
|
300
|
+
tr,
|
|
301
|
+
header,
|
|
302
|
+
content: {
|
|
303
|
+
...(fill ? { fill } : {}),
|
|
304
|
+
...(borders(cs) ? { borders: borders(cs) } : {}),
|
|
305
|
+
padding: {
|
|
306
|
+
top: pt(n(cs.paddingTop) + n(cs.borderTopWidth)),
|
|
307
|
+
left: pt(n(cs.paddingLeft) + n(cs.borderLeftWidth)),
|
|
308
|
+
bottom: pt(n(cs.paddingBottom) + n(cs.borderBottomWidth)),
|
|
309
|
+
right: pt(n(cs.paddingRight) + n(cs.borderRightWidth)),
|
|
310
|
+
},
|
|
311
|
+
vAlign: centred,
|
|
312
|
+
blocks,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const sideBySide = (children) =>
|
|
318
|
+
children.some((a, i) =>
|
|
319
|
+
children.some(
|
|
320
|
+
(b, j) =>
|
|
321
|
+
i !== j &&
|
|
322
|
+
a.getBoundingClientRect().right <= b.getBoundingClientRect().left + 1 &&
|
|
323
|
+
Math.abs(a.getBoundingClientRect().top - b.getBoundingClientRect().top) <
|
|
324
|
+
Math.max(a.getBoundingClientRect().height, 1)
|
|
325
|
+
)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const listBlocks = (list, level, frame, out) => {
|
|
329
|
+
const ordered = list.localName === 'ol';
|
|
330
|
+
lists += 1;
|
|
331
|
+
const instance = lists;
|
|
332
|
+
for (const item of list.children) {
|
|
333
|
+
if (item.localName !== 'li' || !visible(item)) continue;
|
|
334
|
+
const cs = gcs(item);
|
|
335
|
+
const marked = cs.listStyleType !== 'none';
|
|
336
|
+
const nested = [...item.children].filter((child) => ['ul', 'ol'].includes(child.localName));
|
|
337
|
+
const box = contentBox(item);
|
|
338
|
+
const bottom = nested.length ? nested[0].getBoundingClientRect().top : box.bottom;
|
|
339
|
+
out.push(
|
|
340
|
+
paragraphOf(item, frame, {
|
|
341
|
+
skip: (child) => ['ul', 'ol'].includes(child.localName),
|
|
342
|
+
top: box.top,
|
|
343
|
+
bottom,
|
|
344
|
+
...(marked ? { list: { kind: ordered ? 'number' : 'bullet', level, instance: ordered ? instance : 0 } } : {}),
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
for (const sub of nested) listBlocks(sub, Math.min(level + 1, 2), frame, out);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const pictureOf = (el, frame) => {
|
|
352
|
+
const rect = el.getBoundingClientRect();
|
|
353
|
+
const tag = el.localName;
|
|
354
|
+
const picture = {
|
|
355
|
+
kind: 'image',
|
|
356
|
+
width: pt(rect.width),
|
|
357
|
+
height: pt(rect.height),
|
|
358
|
+
indent: Math.max(0, pt(rect.left - frame.left)),
|
|
359
|
+
alt: el.getAttribute('alt') || el.getAttribute('aria-label') || el.dataset?.alt || '',
|
|
360
|
+
top: rect.top,
|
|
361
|
+
bottom: rect.bottom,
|
|
362
|
+
};
|
|
363
|
+
if (tag === 'img' && !/^data:image\/svg/.test(el.currentSrc || '')) {
|
|
364
|
+
picture.src = el.currentSrc || el.getAttribute('src') || '';
|
|
365
|
+
} else {
|
|
366
|
+
captures += 1;
|
|
367
|
+
el.setAttribute('data-docx-capture', String(captures));
|
|
368
|
+
picture.capture = captures;
|
|
369
|
+
}
|
|
370
|
+
// A picture centred in its column is set centred, so it stays centred when the reader edits the margins.
|
|
371
|
+
if (
|
|
372
|
+
Math.abs(rect.left - frame.left - (frame.right - rect.right)) <= 2 &&
|
|
373
|
+
rect.width < frame.right - frame.left - 4
|
|
374
|
+
) {
|
|
375
|
+
picture.align = 'center';
|
|
376
|
+
picture.indent = 0;
|
|
377
|
+
}
|
|
378
|
+
return picture;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// The blocks of one container, in order, each carrying its top and bottom in px for the spacing pass.
|
|
382
|
+
function flow(container, frame, out) {
|
|
383
|
+
let pendingBreak = false;
|
|
384
|
+
const anonymous = [];
|
|
385
|
+
const flushText = () => {
|
|
386
|
+
if (!anonymous.length) return;
|
|
387
|
+
const range = document.createRange();
|
|
388
|
+
range.setStartBefore(anonymous[0]);
|
|
389
|
+
range.setEndAfter(anonymous.at(-1));
|
|
390
|
+
const textRect = range.getBoundingClientRect();
|
|
391
|
+
const runs = [];
|
|
392
|
+
for (const node of anonymous) {
|
|
393
|
+
if (node.nodeType === 3) {
|
|
394
|
+
const text = node.textContent.replace(/[\t\n\r ]+/g, ' ');
|
|
395
|
+
if (text.trim()) {
|
|
396
|
+
const { transform: _t, ...format } = runStyle(container);
|
|
397
|
+
runs.push({ text, ...format });
|
|
398
|
+
}
|
|
399
|
+
} else collectRuns({ childNodes: [node] }, runs);
|
|
400
|
+
}
|
|
401
|
+
anonymous.length = 0;
|
|
402
|
+
const tidy = tidyRuns(runs);
|
|
403
|
+
if (!tidy.length) return;
|
|
404
|
+
const cs = gcs(container);
|
|
405
|
+
out.push({
|
|
406
|
+
kind: 'p',
|
|
407
|
+
tag: 'p',
|
|
408
|
+
runs: tidy,
|
|
409
|
+
align: alignOf(cs),
|
|
410
|
+
line: lineOf(cs),
|
|
411
|
+
size: pt(n(cs.fontSize)),
|
|
412
|
+
indentLeft: 0,
|
|
413
|
+
indentRight: 0,
|
|
414
|
+
firstLine: 0,
|
|
415
|
+
top: textRect.top,
|
|
416
|
+
bottom: textRect.bottom,
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
for (const node of container.childNodes) {
|
|
420
|
+
if (node.nodeType === 3) {
|
|
421
|
+
if (node.textContent.trim()) anonymous.push(node);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (node.nodeType !== 1 || !visible(node)) continue;
|
|
425
|
+
const el = node;
|
|
426
|
+
const cs = gcs(el);
|
|
427
|
+
if (!BLOCK.has(cs.display) && !['table', 'ul', 'ol', 'hr', 'img', 'svg', 'canvas'].includes(el.localName)) {
|
|
428
|
+
anonymous.push(el);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
flushText();
|
|
432
|
+
if (checkUnsupported(el, cs)) continue;
|
|
433
|
+
const before = out.length;
|
|
434
|
+
const breakHere = pendingBreak || cs.breakBefore === 'page' || cs.pageBreakBefore === 'always';
|
|
435
|
+
pendingBreak = cs.breakAfter === 'page' || cs.pageBreakAfter === 'always';
|
|
436
|
+
const tag = el.localName;
|
|
437
|
+
if (tag === 'img' || tag === 'svg' || tag === 'canvas' || el.hasAttribute('data-docx-picture'))
|
|
438
|
+
out.push(pictureOf(el, frame));
|
|
439
|
+
else if (tag === 'table') {
|
|
440
|
+
const cells = [];
|
|
441
|
+
const head = el.tHead;
|
|
442
|
+
for (const row of el.rows) {
|
|
443
|
+
if (!visible(row)) continue;
|
|
444
|
+
for (const cell of row.cells)
|
|
445
|
+
if (visible(cell)) cells.push(cellOf(cell, { tr: row, header: row.parentElement === head }));
|
|
446
|
+
}
|
|
447
|
+
if (cells.length) {
|
|
448
|
+
const table = gridTable(cells, frame, { data: true });
|
|
449
|
+
table.keepTogether = cs.breakInside === 'avoid';
|
|
450
|
+
out.push(table);
|
|
451
|
+
}
|
|
452
|
+
} else if (tag === 'ul' || tag === 'ol') listBlocks(el, 0, frame, out);
|
|
453
|
+
else if (tag === 'hr') {
|
|
454
|
+
const rect = el.getBoundingClientRect();
|
|
455
|
+
const rule = side(cs, 'Top') ||
|
|
456
|
+
side(cs, 'Bottom') || { width: 0.75, color: hex(cs.color) || '999999', style: 'single' };
|
|
457
|
+
out.push({
|
|
458
|
+
kind: 'p',
|
|
459
|
+
tag: 'hr',
|
|
460
|
+
runs: [],
|
|
461
|
+
align: 'left',
|
|
462
|
+
line: 1,
|
|
463
|
+
size: 1,
|
|
464
|
+
indentLeft: Math.max(0, pt(rect.left - frame.left)),
|
|
465
|
+
indentRight: Math.max(0, pt(frame.right - rect.right)),
|
|
466
|
+
firstLine: 0,
|
|
467
|
+
borders: { bottom: rule },
|
|
468
|
+
top: rect.top,
|
|
469
|
+
bottom: rect.bottom,
|
|
470
|
+
});
|
|
471
|
+
} else {
|
|
472
|
+
const kids = blockChildren(el);
|
|
473
|
+
const row =
|
|
474
|
+
/flex/.test(cs.display) && !/column/.test(cs.flexDirection) ? kids : cs.display === 'grid' ? kids : null;
|
|
475
|
+
if (row && row.length >= 2 && sideBySide(row)) {
|
|
476
|
+
// A row of cards: the container's own fill or border wraps the grid as an outer one-cell table.
|
|
477
|
+
const table = gridTable(
|
|
478
|
+
row.map((child) => cellOf(child)),
|
|
479
|
+
contentBox(el)
|
|
480
|
+
);
|
|
481
|
+
if (decorated(cs)) {
|
|
482
|
+
out.push(gridTable([cellOf(el, { given: [table] })], frame));
|
|
483
|
+
} else {
|
|
484
|
+
table.indent = Math.max(0, table.indent + pt(contentBox(el).left - frame.left));
|
|
485
|
+
out.push(table);
|
|
486
|
+
}
|
|
487
|
+
} else if (decorated(cs) && (kids.length || n(cs.paddingTop) + n(cs.paddingLeft) > 3)) {
|
|
488
|
+
out.push(gridTable([cellOf(el)], frame));
|
|
489
|
+
} else if (kids.length) {
|
|
490
|
+
flow(el, frame, out);
|
|
491
|
+
} else {
|
|
492
|
+
const paragraph = paragraphOf(el, frame);
|
|
493
|
+
if (paragraph.runs.length || paragraph.borders || paragraph.shading) out.push(paragraph);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (breakHere && out.length > before) out[before].pageBreakBefore = true;
|
|
497
|
+
}
|
|
498
|
+
flushText();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Space between blocks as the browser laid it: a paragraph takes the gap above it as its own space before; a table
|
|
502
|
+
// has none, so the gap above it goes to the paragraph before it, or to a spacer paragraph.
|
|
503
|
+
function spaceBlocks(blocks, top) {
|
|
504
|
+
const spaced = [];
|
|
505
|
+
let previousBottom = top;
|
|
506
|
+
for (const block of blocks) {
|
|
507
|
+
const gap = pt(Math.max(0, block.top - previousBottom));
|
|
508
|
+
if (block.kind === 'table') {
|
|
509
|
+
const previous = spaced.at(-1);
|
|
510
|
+
if (gap > 0.5 && previous && previous.kind !== 'table') previous.after = (previous.after || 0) + gap;
|
|
511
|
+
else if (gap > 0.5) spaced.push({ kind: 'spacer', height: gap });
|
|
512
|
+
} else block.before = gap;
|
|
513
|
+
spaced.push(block);
|
|
514
|
+
previousBottom = Math.max(previousBottom, block.bottom);
|
|
515
|
+
}
|
|
516
|
+
blocks.splice(0, blocks.length, ...spaced);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const running = (tag) => {
|
|
520
|
+
const el = [...document.body.children].find((child) => child.localName === tag);
|
|
521
|
+
if (!el) return null;
|
|
522
|
+
const textEl =
|
|
523
|
+
[...el.querySelectorAll('*')].find(
|
|
524
|
+
(child) =>
|
|
525
|
+
child.childNodes.length &&
|
|
526
|
+
[...child.childNodes].some((node) => node.nodeType === 3 && node.textContent.trim())
|
|
527
|
+
) || el;
|
|
528
|
+
const cs = gcs(textEl);
|
|
529
|
+
const result = {
|
|
530
|
+
text: (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim(),
|
|
531
|
+
font: face(cs),
|
|
532
|
+
size: pt(n(cs.fontSize)),
|
|
533
|
+
color: hex(cs.color) || '000000',
|
|
534
|
+
bold: (Number(cs.fontWeight) || 400) >= 600,
|
|
535
|
+
align: alignOf(cs),
|
|
536
|
+
firstPage: el.dataset.firstPage || el.getAttribute('data-first-page') || '',
|
|
537
|
+
};
|
|
538
|
+
el.style.setProperty('display', 'none', 'important');
|
|
539
|
+
return result;
|
|
540
|
+
};
|
|
541
|
+
const header = running('header');
|
|
542
|
+
const footer = running('footer');
|
|
543
|
+
const bodyStyle = gcs(document.body);
|
|
544
|
+
const frame = contentBox(document.body);
|
|
545
|
+
const blocks = [];
|
|
546
|
+
flow(document.body, frame, blocks);
|
|
547
|
+
spaceBlocks(blocks, frame.top);
|
|
548
|
+
return {
|
|
549
|
+
blocks,
|
|
550
|
+
header,
|
|
551
|
+
footer,
|
|
552
|
+
body: {
|
|
553
|
+
font: face(bodyStyle),
|
|
554
|
+
size: pt(n(bodyStyle.fontSize)),
|
|
555
|
+
color: hex(bodyStyle.color) || '000000',
|
|
556
|
+
line: lineOf(bodyStyle),
|
|
557
|
+
},
|
|
558
|
+
notes,
|
|
559
|
+
captures,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The `author` action's Word input: lay the HTML out in a local browser at the printable width of its @page sheet,
|
|
2
|
+
// read it back as Word's flow (docx-html-measure.mjs), photograph what Word cannot draw as text (an SVG, a chart, a
|
|
3
|
+
// canvas), print the same HTML as the reference the review compares against, and write the .docx.
|
|
4
|
+
import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { htmlFailure, launchHtmlBrowser, photographCaptures, runInBrowser } from './html-browser.mjs';
|
|
10
|
+
import { printableArea, readPageRule } from './pdf-html-runner.mjs';
|
|
11
|
+
import { iconTable, injectIcons } from './pptx-html-measure.mjs';
|
|
12
|
+
import { drawDataCharts } from './pdf-html-charts.mjs';
|
|
13
|
+
import { extractDocxFlow } from './docx-html-measure.mjs';
|
|
14
|
+
import { buildDocxFromFlow } from './docx-html-build.mjs';
|
|
15
|
+
import { renderPdfPages } from '../pdf/pdf-render.mjs';
|
|
16
|
+
|
|
17
|
+
const PX_TO_PT = 0.75;
|
|
18
|
+
|
|
19
|
+
/** Artifacts kept beside the document: its HTML and the browser's print of it. */
|
|
20
|
+
export function docxHtmlArtifacts(target) {
|
|
21
|
+
return {
|
|
22
|
+
source: `${target}.mixdog-source.html`,
|
|
23
|
+
print: `${target}.mixdog-html.pdf`,
|
|
24
|
+
compare: (page) => `${target}.mixdog-compare-page-${page}.png`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} html the document
|
|
30
|
+
* @param {string} output where the .docx is written (a staging file beside the target)
|
|
31
|
+
* @param {{ target: string, signal?: AbortSignal, timeoutMs?: number }} options
|
|
32
|
+
*/
|
|
33
|
+
export async function runDocxHtmlAuthoring(html, output, { target, signal = null, timeoutMs = 90_000 }) {
|
|
34
|
+
const startedAt = performance.now();
|
|
35
|
+
const artifacts = docxHtmlArtifacts(target);
|
|
36
|
+
await mkdir(dirname(output), { recursive: true });
|
|
37
|
+
await rm(output, { force: true });
|
|
38
|
+
await writeFile(artifacts.source, html, 'utf8');
|
|
39
|
+
const scratch = await mkdtemp(join(tmpdir(), 'mixdog-docx-captures-'));
|
|
40
|
+
let browser;
|
|
41
|
+
try {
|
|
42
|
+
browser = await launchHtmlBrowser('docx-html-browser');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
45
|
+
return htmlFailure(error, startedAt);
|
|
46
|
+
}
|
|
47
|
+
const work = async () => {
|
|
48
|
+
const page = await browser.newPage();
|
|
49
|
+
await page.goto(pathToFileURL(artifacts.source).href, { waitUntil: 'load' });
|
|
50
|
+
const area = printableArea(await page.evaluate(readPageRule));
|
|
51
|
+
// The body is the page's text column: as wide as the sheet between its margins, from the top of that column.
|
|
52
|
+
await page.setViewport({ width: Math.round(area.width), height: Math.round(area.height), deviceScaleFactor: 2 });
|
|
53
|
+
await page.addStyleTag({ content: 'html, body { margin: 0 !important; }' });
|
|
54
|
+
await page.evaluate(injectIcons, iconTable().table, '0 0 24 24');
|
|
55
|
+
const charts = await page.evaluate(drawDataCharts);
|
|
56
|
+
await page.evaluate(() => document.fonts.ready);
|
|
57
|
+
const flow = await page.evaluate(extractDocxFlow);
|
|
58
|
+
const captures = await photographCaptures(page, 'data-docx-capture', flow.captures, scratch);
|
|
59
|
+
// The reference the review holds the Word render against: the same HTML as print lays it out.
|
|
60
|
+
await page.emulateMediaType('print');
|
|
61
|
+
await page.pdf({ path: artifacts.print, preferCSSPageSize: true, printBackground: true });
|
|
62
|
+
const title = await page.title();
|
|
63
|
+
return { area, flow, captures, chartWarnings: charts.warnings, title };
|
|
64
|
+
};
|
|
65
|
+
let laid;
|
|
66
|
+
try {
|
|
67
|
+
laid = await runInBrowser(browser, { signal, timeoutMs }, work);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (signal?.aborted) throw error;
|
|
70
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
71
|
+
return htmlFailure(error, startedAt);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const printed = await renderPdfPages(artifacts.print, { maxWidth: 900 }).catch(() => null);
|
|
75
|
+
const points = (value) => Math.round(value * PX_TO_PT * 100) / 100;
|
|
76
|
+
const built = await buildDocxFromFlow(laid.flow, output, {
|
|
77
|
+
page: {
|
|
78
|
+
sheet: { width: points(laid.area.sheet.width), height: points(laid.area.sheet.height) },
|
|
79
|
+
margin: Object.fromEntries(Object.entries(laid.area.margin).map(([side, value]) => [side, points(value)])),
|
|
80
|
+
},
|
|
81
|
+
captures: laid.captures,
|
|
82
|
+
title: laid.title,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
output,
|
|
87
|
+
bytes: (await stat(output)).size,
|
|
88
|
+
kit: 'html',
|
|
89
|
+
htmlSource: artifacts.source,
|
|
90
|
+
htmlPages: (printed?.images || []).map((image) => ({ page: image.page, path: image.path })),
|
|
91
|
+
logs: [...built.notes, ...laid.chartWarnings.map((warning) => warning.message)].map((text) => ({
|
|
92
|
+
level: 'warn',
|
|
93
|
+
text,
|
|
94
|
+
})),
|
|
95
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return htmlFailure(error, startedAt);
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
}
|