mixdog 1.0.3 → 1.0.5

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.
Files changed (160) hide show
  1. package/README.md +10 -5
  2. package/package.json +18 -17
  3. package/scripts/gate-local.mjs +93 -0
  4. package/scripts/lib/gate-scheduler.mjs +24 -0
  5. package/scripts/lib/run-node-tests.mjs +5 -1
  6. package/scripts/release-paths.mjs +22 -1
  7. package/scripts/test-direct.mjs +2 -1
  8. package/scripts/test.mjs +3 -1
  9. package/src/defaults/agents.json +0 -12
  10. package/src/defaults/mixdog-config.template.json +1 -2
  11. package/src/defaults/skills/docx/SKILL.md +7 -4
  12. package/src/defaults/skills/docx/references/html.md +46 -0
  13. package/src/defaults/skills/docx/references/native-authoring.md +18 -4
  14. package/src/defaults/skills/goal-management/SKILL.md +2 -2
  15. package/src/defaults/skills/pdf/SKILL.md +10 -2
  16. package/src/defaults/skills/pdf/references/html.md +64 -0
  17. package/src/defaults/skills/pptx/references/html.md +1 -1
  18. package/src/defaults/skills/xlsx/SKILL.md +7 -3
  19. package/src/defaults/skills/xlsx/references/html.md +40 -0
  20. package/src/defaults/skills/xlsx/references/report-design.md +21 -4
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  23. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
  28. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
  29. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
  30. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
  31. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
  32. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
  33. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  35. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
  36. package/src/runtime/attachments/pdf-extract.mjs +7 -3
  37. package/src/runtime/computer-bridge/client.mjs +1 -1
  38. package/src/runtime/memory/index.mjs +4 -15
  39. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  40. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
  41. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
  42. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
  43. package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
  44. package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
  45. package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
  46. package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
  47. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
  48. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
  49. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
  50. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  51. package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
  52. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
  53. package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
  54. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
  55. package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
  56. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
  57. package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
  58. package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
  59. package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
  60. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  61. package/src/runtime/memory/lib/memory-embed.mjs +14 -1
  62. package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
  63. package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
  64. package/src/runtime/memory/lib/memory.mjs +2 -6
  65. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  66. package/src/runtime/memory/lib/query-handlers.mjs +1 -1
  67. package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
  68. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
  69. package/src/runtime/memory/lib/recall-format.mjs +1 -1
  70. package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
  71. package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
  72. package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
  73. package/src/runtime/office/authoring/html-browser.mjs +61 -0
  74. package/src/runtime/office/authoring/html-document-author.mjs +181 -0
  75. package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
  76. package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
  77. package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
  78. package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
  79. package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
  80. package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
  81. package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
  82. package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
  83. package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
  84. package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
  85. package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
  86. package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
  87. package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
  88. package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
  89. package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
  90. package/src/runtime/office/capabilities-catalog.mjs +8 -0
  91. package/src/runtime/office/capabilities-signatures.mjs +18 -6
  92. package/src/runtime/office/capabilities.mjs +26 -1
  93. package/src/runtime/office/com/office-com-host.ps1 +133 -17
  94. package/src/runtime/office/core/office-actions-batch.mjs +16 -1
  95. package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
  96. package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
  97. package/src/runtime/office/core/office-actions-render.mjs +4 -1
  98. package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
  99. package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
  100. package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
  101. package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
  102. package/src/runtime/office/core/office-sessions.mjs +19 -2
  103. package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
  104. package/src/runtime/office/portable/docx-formatting.mjs +21 -0
  105. package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
  106. package/src/runtime/office/portable/portable-chart.mjs +10 -2
  107. package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
  108. package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
  109. package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
  110. package/src/runtime/office/portable/portable-docx.mjs +2 -0
  111. package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
  112. package/src/runtime/office/portable/portable-validation.mjs +6 -3
  113. package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
  114. package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
  115. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
  116. package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
  117. package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
  118. package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
  119. package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
  120. package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
  121. package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
  122. package/src/runtime/office/quality/design-review-critique.mjs +3 -2
  123. package/src/runtime/office/quality/document-brief.mjs +44 -0
  124. package/src/runtime/office/quality/inline-audit.mjs +7 -2
  125. package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
  126. package/src/runtime/office/tool-defs.mjs +2 -2
  127. package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
  128. package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
  129. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
  130. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  131. package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
  132. package/src/runtime/shared/provider-accounts.mjs +11 -1
  133. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
  134. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  135. package/src/runtime/shared/wait-until.test-support.mjs +33 -0
  136. package/src/session-runtime/boot/apis.mjs +1 -1
  137. package/src/session-runtime/services/channel-admin.mjs +4 -3
  138. package/src/session-runtime/services/usage-stats-model.mjs +22 -0
  139. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
  140. package/src/session-runtime/turn/session-ops.mjs +15 -9
  141. package/src/session-runtime/usage-stats-api.mjs +34 -2
  142. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  143. package/src/standalone/local-session-runtime.mjs +488 -0
  144. package/src/standalone/session-protocol.mjs +1 -0
  145. package/src/standalone/session-runtime-inline-host.mjs +1 -1
  146. package/src/tui/hooks/useSession.mjs +1 -1
  147. package/src/tui/session/session-api/commands.mjs +1 -1
  148. package/src/tui/session/session-api/integrations.mjs +3 -0
  149. package/src/tui/session/session-flow/auto-clear.mjs +6 -7
  150. package/src/tui/session/tool-card-results.mjs +1 -1
  151. package/src/tui/session/turn.mjs +5 -0
  152. package/src/tui/session-local.mjs +3 -488
  153. package/src/tui/session.mjs +1 -1
  154. package/src/ui/statusline-agents.mjs +0 -1
  155. package/src/rules/agent/41-cycle2-agent.md +0 -24
  156. package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
  157. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
  158. package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
  159. package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
  160. package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
@@ -0,0 +1,375 @@
1
+ // The Word HTML route, second half: the flow docx-html-measure.mjs read from the browser becomes a native Word
2
+ // document — the page and running lines through the portable writer's own operations, the body as WordprocessingML
3
+ // written block by block: paragraphs in their runs and spacing, tables on the grid the browser drew (data tables,
4
+ // rows of cards, boxes), lists on Word numbering, and pictures. Every number is points; the body follows the page's
5
+ // text width, so a block the browser set 40 pt in sits 40 pt in on the Word page.
6
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ import { createPortableOoxmlDocument } from '../portable/portable-package.mjs';
12
+ import { applyPortableOoxmlBatch } from '../portable/portable-ooxml.mjs';
13
+ import { loadPackage, savePackage } from '../portable/portable-opc.mjs';
14
+ import { appendDocxBlock } from '../portable/portable-snapshot.mjs';
15
+ import { addDocumentImage, ensureNumbering, wordDrawingXml } from '../portable/portable-docx-parts.mjs';
16
+ import { wordRunProperties, wordTextContent } from '../portable/portable-docx-xml.mjs';
17
+ import { documentStyleId } from '../portable/portable-docx-styles.mjs';
18
+ import { xmlEncode } from '../portable/portable-xml.mjs';
19
+
20
+ const tw = (points) => Math.max(0, Math.round(Number(points || 0) * 20));
21
+ const eighths = (points) => Math.max(2, Math.min(96, Math.round(Number(points || 0.75) * 8)));
22
+ const BORDER_STYLE = { single: 'single', dashed: 'dashed', dotted: 'dotted', double: 'double' };
23
+ const SIDES = ['top', 'left', 'bottom', 'right'];
24
+
25
+ const borderElement = (tag, border, space = 0) =>
26
+ `<w:${tag} w:val="${BORDER_STYLE[border.style] || 'single'}" w:sz="${eighths(border.width)}" w:space="${Math.max(0, Math.min(31, Math.round(space)))}" w:color="${border.color}"/>`;
27
+
28
+ function runsXml(runs) {
29
+ return runs
30
+ .map((run) => {
31
+ if (run.br) return '<w:r><w:br/></w:r>';
32
+ const properties = [
33
+ wordRunProperties({
34
+ name: run.font || undefined,
35
+ nameEastAsia: run.font || undefined,
36
+ size: run.size,
37
+ bold: run.bold || undefined,
38
+ italic: run.italic || undefined,
39
+ underline: run.underline || undefined,
40
+ color: run.color,
41
+ }),
42
+ run.strike ? '<w:strike/>' : '',
43
+ run.shading ? `<w:shd w:val="clear" w:color="auto" w:fill="${run.shading}"/>` : '',
44
+ run.vertAlign ? `<w:vertAlign w:val="${run.vertAlign}"/>` : '',
45
+ ].join('');
46
+ return `<w:r>${properties ? `<w:rPr>${properties}</w:rPr>` : ''}${wordTextContent(run.text, { preserve: true })}</w:r>`;
47
+ })
48
+ .join('');
49
+ }
50
+
51
+ // Paragraph properties in the schema's order: style, keeps, break, numbering, borders, shading, spacing, indents,
52
+ // justification.
53
+ function paragraphProperties(block, { styleId = '', numbering = null, exactLine = true } = {}) {
54
+ const parts = [];
55
+ if (styleId) parts.push(`<w:pStyle w:val="${xmlEncode(styleId)}"/>`);
56
+ if (block.keepNext) parts.push('<w:keepNext/>');
57
+ if (block.keepLines) parts.push('<w:keepLines/>');
58
+ if (block.pageBreakBefore) parts.push('<w:pageBreakBefore/>');
59
+ if (numbering)
60
+ parts.push(`<w:numPr><w:ilvl w:val="${numbering.level}"/><w:numId w:val="${numbering.numId}"/></w:numPr>`);
61
+ if (block.borders) {
62
+ const sides = SIDES.filter((side) => block.borders[side]);
63
+ if (sides.length) {
64
+ parts.push(
65
+ `<w:pBdr>${sides.map((side) => borderElement(side, block.borders[side], block.borderSpace?.[side] ?? 1)).join('')}</w:pBdr>`
66
+ );
67
+ }
68
+ }
69
+ if (block.shading) parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${block.shading}"/>`);
70
+ const line = exactLine && block.line > 0 ? ` w:line="${tw(block.line)}" w:lineRule="exact"` : '';
71
+ parts.push(`<w:spacing w:before="${tw(block.before)}" w:after="${tw(block.after)}"${line}/>`);
72
+ const hanging = numbering ? Math.min(18, Math.max(0, block.indentLeft)) : 0;
73
+ const first = numbering
74
+ ? hanging
75
+ ? ` w:hanging="${tw(hanging)}"`
76
+ : ''
77
+ : block.firstLine > 0
78
+ ? ` w:firstLine="${tw(block.firstLine)}"`
79
+ : block.firstLine < 0
80
+ ? ` w:hanging="${tw(-block.firstLine)}"`
81
+ : '';
82
+ if (block.indentLeft || block.indentRight || first) {
83
+ parts.push(`<w:ind w:left="${tw(block.indentLeft)}" w:right="${tw(block.indentRight)}"${first}/>`);
84
+ }
85
+ if (block.align && block.align !== 'left') parts.push(`<w:jc w:val="${block.align}"/>`);
86
+ return `<w:pPr>${parts.join('')}</w:pPr>`;
87
+ }
88
+
89
+ const tinyParagraph = (height = 1, pageBreakBefore = false) =>
90
+ `<w:p><w:pPr>${pageBreakBefore ? '<w:pageBreakBefore/>' : ''}<w:spacing w:before="0" w:after="0" w:line="${tw(height)}" w:lineRule="exact"/><w:rPr><w:sz w:val="2"/><w:szCs w:val="2"/></w:rPr></w:pPr></w:p>`;
91
+
92
+ const cellBorders = (borders) => {
93
+ if (!borders) return '';
94
+ return `<w:tcBorders>${SIDES.map((side) => (borders[side] ? borderElement(side, borders[side]) : `<w:${side} w:val="nil"/>`)).join('')}</w:tcBorders>`;
95
+ };
96
+
97
+ // Only the side margins are the cell's: Word sets a row's text at its deepest top margin in every cell of the row, so a
98
+ // card's padding pushed the unpadded column beside it down. The top and bottom padding become the space before the
99
+ // cell's first block and after its last (cellBlocks).
100
+ const cellMargins = (padding) =>
101
+ padding
102
+ ? `<w:tcMar><w:top w:w="0" w:type="dxa"/><w:left w:w="${tw(padding.left)}" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="${tw(padding.right)}" w:type="dxa"/></w:tcMar>`
103
+ : '';
104
+
105
+ function cellBlocks(cell) {
106
+ const blocks = cell.blocks.map((block) => ({ ...block }));
107
+ const top = cell.padding?.top || 0;
108
+ const bottom = cell.padding?.bottom || 0;
109
+ const paragraph = (block) => block && block.kind !== 'table' && block.kind !== 'spacer';
110
+ if (top > 0.5) {
111
+ if (paragraph(blocks[0])) blocks[0].before = (blocks[0].before || 0) + top;
112
+ else blocks.unshift({ kind: 'spacer', height: top });
113
+ }
114
+ if (bottom > 0.5) {
115
+ if (paragraph(blocks.at(-1))) blocks.at(-1).after = (blocks.at(-1).after || 0) + bottom;
116
+ else blocks.push({ kind: 'spacer', height: bottom });
117
+ }
118
+ return blocks;
119
+ }
120
+
121
+ /** Writes a flow (docx-html-measure.mjs) into the body of the document at `path`. */
122
+ class BodyWriter {
123
+ constructor(zip, { captures, notes }) {
124
+ this.zip = zip;
125
+ this.captures = captures;
126
+ this.notes = notes;
127
+ this.drawingId = 1000;
128
+ this.lists = new Map();
129
+ this.bullet = null;
130
+ this.headingStyles = new Map();
131
+ this.scratch = null;
132
+ }
133
+
134
+ async numbering(list) {
135
+ if (list.kind === 'bullet') {
136
+ this.bullet ||= (await ensureNumbering(this.zip, 'bullet')).numId;
137
+ return { numId: this.bullet, level: list.level };
138
+ }
139
+ if (!this.lists.has(list.instance)) {
140
+ const restart = this.lists.size > 0;
141
+ this.lists.set(list.instance, (await ensureNumbering(this.zip, 'number', { restart })).numId);
142
+ }
143
+ return { numId: this.lists.get(list.instance), level: list.level };
144
+ }
145
+
146
+ async headingStyle(level) {
147
+ if (!this.headingStyles.has(level)) {
148
+ this.headingStyles.set(level, (await documentStyleId(this.zip, `Heading ${level}`)).id);
149
+ }
150
+ return this.headingStyles.get(level);
151
+ }
152
+
153
+ async picturePath(block) {
154
+ if (block.capture) return this.captures.get(block.capture) || '';
155
+ const src = String(block.src || '');
156
+ if (src.startsWith('file:')) return fileURLToPath(src);
157
+ const data = /^data:image\/(png|jpe?g|gif);base64,(.+)$/i.exec(src);
158
+ if (data) {
159
+ this.scratch ||= await mkdtemp(join(tmpdir(), 'mixdog-docx-html-'));
160
+ const path = join(this.scratch, `inline-${this.drawingId}.${data[1].toLowerCase().replace('jpeg', 'jpg')}`);
161
+ await writeFile(path, Buffer.from(data[2], 'base64'));
162
+ return path;
163
+ }
164
+ this.notes.push(`picture ${src.slice(0, 60)} is not a local file or data: URI; it was left out`);
165
+ return '';
166
+ }
167
+
168
+ async block(block) {
169
+ if (block.kind === 'spacer') return tinyParagraph(block.height);
170
+ if (block.kind === 'table') {
171
+ // Word puts page breaks on paragraphs, not on tables.
172
+ return (block.pageBreakBefore ? tinyParagraph(1, true) : '') + (await this.table(block));
173
+ }
174
+ if (block.kind === 'image') return this.picture(block);
175
+ const numbering = block.list ? await this.numbering(block.list) : null;
176
+ const styleId = block.heading && block.heading <= 3 ? await this.headingStyle(block.heading) : '';
177
+ return `<w:p>${paragraphProperties(block, { styleId, numbering })}${runsXml(block.runs)}</w:p>`;
178
+ }
179
+
180
+ async picture(block) {
181
+ const path = await this.picturePath(block);
182
+ if (!path) return '';
183
+ const image = await addDocumentImage(this.zip, path);
184
+ this.drawingId += 1;
185
+ const drawing = wordDrawingXml({
186
+ id: this.drawingId,
187
+ embedId: image.relationshipId,
188
+ name: image.name,
189
+ width: block.width,
190
+ height: block.height,
191
+ altText: block.alt,
192
+ });
193
+ const properties = paragraphProperties(
194
+ { ...block, indentLeft: block.indent, indentRight: 0, firstLine: 0, align: block.align || 'left' },
195
+ { exactLine: false }
196
+ );
197
+ return `<w:p>${properties}<w:r>${drawing}</w:r></w:p>`;
198
+ }
199
+
200
+ async blocks(blocks, { keepAll = false } = {}) {
201
+ const xml = [];
202
+ for (const [index, block] of blocks.entries()) {
203
+ const next = blocks[index + 1];
204
+ // A short paragraph right before a table or picture is its title (그림 1. …, 표 1. …): it goes with it to the
205
+ // next page, as Word keeps a caption with its table. keepAll holds a row's content to the row after it.
206
+ const caption =
207
+ block.kind === 'p' &&
208
+ (next?.kind === 'table' || next?.kind === 'image') &&
209
+ block.runs.reduce((length, run) => length + (run.text?.length || 0), 0) <= 80;
210
+ xml.push(await this.block(keepAll || caption ? { ...block, keepNext: true } : block));
211
+ }
212
+ return xml.join('');
213
+ }
214
+
215
+ async table(table) {
216
+ const total = table.columns.reduce((sum, width) => sum + width, 0);
217
+ const properties =
218
+ `<w:tblW w:w="${tw(total)}" w:type="dxa"/>` +
219
+ (table.indent ? `<w:tblInd w:w="${tw(table.indent)}" w:type="dxa"/>` : '') +
220
+ `<w:tblBorders>${[...SIDES, 'insideH', 'insideV'].map((side) => `<w:${side} w:val="nil"/>`).join('')}</w:tblBorders>` +
221
+ '<w:tblLayout w:type="fixed"/>' +
222
+ `<w:tblCellMar>${SIDES.map((side) => `<w:${side} w:w="0" w:type="dxa"/>`).join('')}</w:tblCellMar>`;
223
+ const grid = table.columns.map((width) => `<w:gridCol w:w="${tw(width)}"/>`).join('');
224
+ const rows = [];
225
+ // A short table (six rows or fewer), one asked to stay whole, or a layout grid moves to the next page whole: each
226
+ // row's text keeps with the row after it. A long data table breaks between rows, its header repeated.
227
+ const contentRows = table.rows.filter((row) => !row.spacer);
228
+ const keepWhole = !table.data || table.keepTogether || contentRows.length <= 6;
229
+ const lastRow = contentRows.at(-1);
230
+ for (const row of table.rows) {
231
+ if (row.spacer) {
232
+ rows.push(
233
+ `<w:tr><w:trPr><w:cantSplit/><w:trHeight w:val="${tw(row.height)}" w:hRule="exact"/></w:trPr>` +
234
+ `<w:tc><w:tcPr><w:tcW w:w="${tw(total)}" w:type="dxa"/>${table.columns.length > 1 ? `<w:gridSpan w:val="${table.columns.length}"/>` : ''}</w:tcPr>${tinyParagraph()}</w:tc></w:tr>`
235
+ );
236
+ continue;
237
+ }
238
+ // A layout row (cards) stays whole; a data table breaks between rows as Word breaks it, its header repeated.
239
+ const height = row.height - 0.5;
240
+ const trPr =
241
+ (table.data && !table.keepTogether ? '' : '<w:cantSplit/>') +
242
+ (height > 1 ? `<w:trHeight w:val="${tw(height)}" w:hRule="atLeast"/>` : '') +
243
+ (row.header ? '<w:tblHeader/>' : '');
244
+ const cells = [];
245
+ let column = 0;
246
+ for (const cell of row.cells) {
247
+ const width = table.columns.slice(column, column + cell.span).reduce((sum, value) => sum + value, 0);
248
+ column += cell.span;
249
+ const tcPr =
250
+ `<w:tcW w:w="${tw(width)}" w:type="dxa"/>` +
251
+ (cell.span > 1 ? `<w:gridSpan w:val="${cell.span}"/>` : '') +
252
+ cellBorders(cell.borders) +
253
+ (cell.fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${cell.fill}"/>` : '') +
254
+ cellMargins(cell.padding) +
255
+ `<w:vAlign w:val="${cell.vAlign || 'top'}"/>`;
256
+ let content =
257
+ cell.empty || !cell.blocks?.length
258
+ ? tinyParagraph()
259
+ : await this.blocks(cellBlocks(cell), { keepAll: keepWhole && row !== lastRow });
260
+ if (!/<\/w:p>$/.test(content)) content += tinyParagraph();
261
+ cells.push(`<w:tc><w:tcPr>${tcPr}</w:tcPr>${content}</w:tc>`);
262
+ }
263
+ rows.push(`<w:tr>${trPr ? `<w:trPr>${trPr}</w:trPr>` : ''}${cells.join('')}</w:tr>`);
264
+ }
265
+ return `<w:tbl><w:tblPr>${properties}</w:tblPr><w:tblGrid>${grid}</w:tblGrid>${rows.join('')}</w:tbl>`;
266
+ }
267
+
268
+ async dispose() {
269
+ if (this.scratch) await rm(this.scratch, { recursive: true, force: true }).catch(() => {});
270
+ }
271
+ }
272
+
273
+ // The running line and page numbers as the portable writer's own operations: a footer reading "{page} / {pages}"
274
+ // is the page field and the total, "Page {page}" a prefix; data-first-page="none" leaves the cover without it.
275
+ function runningOperations(kind, running) {
276
+ if (!running?.text) return [];
277
+ const properties = {
278
+ ...(running.font ? { name: running.font, nameEastAsia: running.font } : {}),
279
+ size: running.size,
280
+ color: running.color,
281
+ ...(running.bold ? { bold: true } : {}),
282
+ alignment: running.align,
283
+ };
284
+ const numbered = /^(.*?)\{page\}(?:(.*?)\{pages\})?(.*)$/.exec(running.text);
285
+ const operations = numbered
286
+ ? [
287
+ {
288
+ op: 'add_page_numbers',
289
+ kind,
290
+ prefix: numbered[1].trim(),
291
+ ...(numbered[2] !== undefined
292
+ ? { separator: numbered[2].trim() || '/', includeTotal: true }
293
+ : { includeTotal: false }),
294
+ alignment: running.align,
295
+ },
296
+ ]
297
+ : [{ op: 'set_header_footer', kind, text: running.text, properties }];
298
+ if (['none', 'blank', 'hidden'].includes(String(running.firstPage).toLowerCase())) {
299
+ operations.push({ op: 'set_header_footer', kind, variant: 'first', text: '' });
300
+ }
301
+ return operations;
302
+ }
303
+
304
+ /**
305
+ * Writes the measured flow to `output` as a .docx.
306
+ * @param {object} flow extractDocxFlow's result
307
+ * @param {{ sheet: { width: number, height: number }, margin: object }} page the sheet and margins in points
308
+ * @param {Map<number, string>} captures picture files for captured elements (an SVG, a chart, a canvas)
309
+ */
310
+ export async function buildDocxFromFlow(flow, output, { page, captures, title = '' }) {
311
+ const notes = [...(flow.notes || [])];
312
+ await createPortableOoxmlDocument(output, { fileKind: 'docx', title });
313
+ const firstHeading = (level) => flow.blocks.find((block) => block.heading === level && block.runs?.length);
314
+ const headingStyles = {};
315
+ for (const level of [1, 2, 3]) {
316
+ const block = firstHeading(level);
317
+ const run = block?.runs.find((entry) => entry.text);
318
+ if (!run) continue;
319
+ headingStyles[`Heading ${level}`] = {
320
+ ...(run.font ? { name: run.font, nameEastAsia: run.font } : {}),
321
+ size: run.size,
322
+ bold: run.bold,
323
+ color: run.color,
324
+ keepWithNext: true,
325
+ };
326
+ }
327
+ const body = flow.body || {};
328
+ const face = body.font ? { name: body.font, nameEastAsia: body.font } : {};
329
+ await applyPortableOoxmlBatch(output, 'docx', [
330
+ {
331
+ op: 'set_page',
332
+ properties: {
333
+ pageWidth: page.sheet.width,
334
+ pageHeight: page.sheet.height,
335
+ topMargin: page.margin.top,
336
+ rightMargin: page.margin.right,
337
+ bottomMargin: page.margin.bottom,
338
+ leftMargin: page.margin.left,
339
+ },
340
+ },
341
+ {
342
+ op: 'set_document_font',
343
+ properties: { ...face, ...(body.size ? { size: body.size } : {}), ...(body.color ? { color: body.color } : {}) },
344
+ },
345
+ {
346
+ op: 'define_styles',
347
+ styles: {
348
+ Normal: {
349
+ ...face,
350
+ ...(body.size ? { size: body.size } : {}),
351
+ spacingBefore: 0,
352
+ spacingAfter: 0,
353
+ alignment: 'left',
354
+ },
355
+ ...headingStyles,
356
+ },
357
+ },
358
+ ...runningOperations('header', flow.header),
359
+ ...runningOperations('footer', flow.footer),
360
+ ]);
361
+ const zip = await loadPackage(output);
362
+ const writer = new BodyWriter(zip, { captures, notes });
363
+ try {
364
+ let xml = await writer.blocks(flow.blocks);
365
+ if (!xml) xml = tinyParagraph();
366
+ // The body ends on a paragraph: a document that ends in a table gives Word nowhere to put the cursor.
367
+ if (/<\/w:tbl>$/.test(xml)) xml += tinyParagraph();
368
+ const documentPart = 'word/document.xml';
369
+ zip.file(documentPart, appendDocxBlock(await zip.file(documentPart).async('string'), xml));
370
+ await savePackage(zip, output);
371
+ } finally {
372
+ await writer.dispose();
373
+ }
374
+ return { notes };
375
+ }