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,365 @@
|
|
|
1
|
+
// The Excel HTML route, second half: each measured sheet becomes a worksheet on a column grid cut at every edge the
|
|
2
|
+
// browser drew — a card's border and its text's, a table's cells, a chart's frame — so the layout lands on columns and
|
|
3
|
+
// rows of the widths and heights it had. Texts become values (numbers where a table cell holds a figure, formulas
|
|
4
|
+
// where data-formula names one) in their type and alignment, merged across the cells they span; boxes become fills
|
|
5
|
+
// and rules; charts become native charts over their data; and the page is set to print as one sheet. Everything is
|
|
6
|
+
// written through the portable writer's own operations, so the workbook is ordinary, editable Excel.
|
|
7
|
+
import { createPortableOoxmlDocument } from '../portable/portable-package.mjs';
|
|
8
|
+
import { applyPortableOoxmlBatch } from '../portable/portable-ooxml.mjs';
|
|
9
|
+
import { typedNumber } from '../portable/portable-xlsx-sheet-edits.mjs';
|
|
10
|
+
|
|
11
|
+
const PX_TO_PT = 0.75;
|
|
12
|
+
const SNAP = 2;
|
|
13
|
+
const CHART_DATA_SHEET = '차트 데이터';
|
|
14
|
+
|
|
15
|
+
const columnLabel = (number) => {
|
|
16
|
+
let value = number;
|
|
17
|
+
let label = '';
|
|
18
|
+
while (value > 0) {
|
|
19
|
+
value -= 1;
|
|
20
|
+
label = String.fromCharCode(65 + (value % 26)) + label;
|
|
21
|
+
value = Math.floor(value / 26);
|
|
22
|
+
}
|
|
23
|
+
return label;
|
|
24
|
+
};
|
|
25
|
+
const quoted = (name) => (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${String(name).replaceAll("'", "''")}'`);
|
|
26
|
+
|
|
27
|
+
// Excel's column width is in characters of the workbook's digit (7 px for the default Calibri 11): px = 7w + 5 from
|
|
28
|
+
// one character up, 12 px per character under it.
|
|
29
|
+
const columnCharacters = (px) => (px >= 12 ? (px - 5) / 7 : Math.max(0.08, px / 12));
|
|
30
|
+
|
|
31
|
+
// The distinct edges of every box, snapped: within two pixels is one edge.
|
|
32
|
+
function edges(values, extent) {
|
|
33
|
+
const sorted = [0, ...values, extent].filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const value of sorted) if (!out.length || value - out.at(-1) > SNAP) out.push(value);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const indexOf = (list, value) => {
|
|
39
|
+
let best = 0;
|
|
40
|
+
for (let index = 1; index < list.length; index += 1)
|
|
41
|
+
if (Math.abs(list[index] - value) < Math.abs(list[best] - value)) best = index;
|
|
42
|
+
return best;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// A table cell's text as a value: a figure is a number in the format it was written in ("184,200" → #,##0, "+12.0%"
|
|
46
|
+
// → +0.0%), an ISO date is a date; data-value and data-format override, data-formula writes a formula.
|
|
47
|
+
function cellContent(item, { figures = false } = {}) {
|
|
48
|
+
if (item.formula)
|
|
49
|
+
return { formula: item.formula.startsWith('=') ? item.formula : `=${item.formula}`, format: item.format };
|
|
50
|
+
if (item.value !== undefined) {
|
|
51
|
+
return { value: item.value, format: item.format };
|
|
52
|
+
}
|
|
53
|
+
const text = String(item.text || '');
|
|
54
|
+
if (figures) {
|
|
55
|
+
const figure = typedNumber(text.trim().replace(/^−/, '-'));
|
|
56
|
+
if (figure) {
|
|
57
|
+
const numberText = text.trim();
|
|
58
|
+
const percent = numberText.endsWith('%');
|
|
59
|
+
const [whole, decimals = ''] = numberText.replace(/%$/, '').split('.');
|
|
60
|
+
const { value } = figure;
|
|
61
|
+
const fraction = decimals ? `.${'0'.repeat(decimals.length)}` : '';
|
|
62
|
+
const grouped = whole.includes(',') || Math.abs(value) >= 1000 ? '#,##0' : '0';
|
|
63
|
+
let format = `${percent ? '0' : grouped}${fraction}${percent ? '%' : ''}`;
|
|
64
|
+
if (numberText.startsWith('+')) format = `+${format};-${format};${format}`;
|
|
65
|
+
return { value, format: item.format || format };
|
|
66
|
+
}
|
|
67
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return { value: text, format: item.format || 'yyyy-mm-dd' };
|
|
68
|
+
}
|
|
69
|
+
return { value: text, format: item.format };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A chart's data-chart spec as add_chart reads it. One series with one colour set apart is a highlight.
|
|
73
|
+
function chartOperation(chart, sheet, anchor, range) {
|
|
74
|
+
const spec = chart.spec || {};
|
|
75
|
+
const type =
|
|
76
|
+
{ col: 'column', column: 'column', bar: 'bar', line: 'line', area: 'area', pie: 'pie', doughnut: 'doughnut' }[
|
|
77
|
+
String(spec.type || 'col').toLowerCase()
|
|
78
|
+
] || 'column';
|
|
79
|
+
const colors = Array.isArray(spec.colors) ? spec.colors.map((color) => String(color).replace(/^#/, '')) : [];
|
|
80
|
+
const single = !Array.isArray(spec.series);
|
|
81
|
+
let palette = colors;
|
|
82
|
+
let highlight;
|
|
83
|
+
let muted;
|
|
84
|
+
if (single && colors.length > 1 && !['pie', 'doughnut'].includes(type)) {
|
|
85
|
+
const counts = new Map();
|
|
86
|
+
for (const color of colors) counts.set(color, (counts.get(color) || 0) + 1);
|
|
87
|
+
const odd = colors.findIndex((color) => counts.get(color) === 1);
|
|
88
|
+
if (counts.size === 2 && odd >= 0) {
|
|
89
|
+
highlight = odd;
|
|
90
|
+
palette = [colors[odd]];
|
|
91
|
+
muted = colors.find((color) => color !== colors[odd]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
op: 'add_chart',
|
|
96
|
+
sheet,
|
|
97
|
+
range,
|
|
98
|
+
cell: anchor.cell,
|
|
99
|
+
toColumn: anchor.toColumn,
|
|
100
|
+
height: Math.round(chart.rect.bottom - chart.rect.top) * PX_TO_PT,
|
|
101
|
+
chartType: type,
|
|
102
|
+
...(chart.title ? { title: chart.title } : {}),
|
|
103
|
+
...(palette.length ? { seriesColors: palette } : {}),
|
|
104
|
+
...(highlight !== undefined ? { highlight, mutedColor: muted } : {}),
|
|
105
|
+
showValues: spec.showValues !== false,
|
|
106
|
+
showLegend: Boolean(spec.legend),
|
|
107
|
+
gridlines: Boolean(spec.grid),
|
|
108
|
+
valueAxis: spec.valueAxis === true,
|
|
109
|
+
zeroBaseline: true,
|
|
110
|
+
...(spec.format ? { valueNumberFormat: spec.format } : {}),
|
|
111
|
+
...(chart.font ? { fontName: chart.font } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// `{id}` in a formula or a chart's data-range is the cell the element with that id landed on — qualified with its
|
|
116
|
+
// sheet when the formula stands on another — so the HTML names its cells without knowing the grid they land on.
|
|
117
|
+
const PLACEHOLDER = /\{([A-Za-z][\w-]*)\}/g;
|
|
118
|
+
function resolvePlaceholders(text, sheet, cells, notes) {
|
|
119
|
+
return String(text).replace(PLACEHOLDER, (whole, id) => {
|
|
120
|
+
const cell = cells.get(id);
|
|
121
|
+
if (!cell) {
|
|
122
|
+
notes.push(`{${id}} names no element with that id; it was left as written`);
|
|
123
|
+
return whole;
|
|
124
|
+
}
|
|
125
|
+
return cell.sheet === sheet ? cell.cell : `${quoted(cell.sheet)}!${cell.cell}`;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A chart's range, area by area: an area named by placeholders takes its sheet once, at its front ('보고'!B14:B18).
|
|
130
|
+
function resolveRange(text, cells, notes) {
|
|
131
|
+
return String(text)
|
|
132
|
+
.split(',')
|
|
133
|
+
.map((area) => {
|
|
134
|
+
const ids = [...area.matchAll(PLACEHOLDER)].map((match) => match[1]);
|
|
135
|
+
const sheet = ids.map((id) => cells.get(id)?.sheet).find(Boolean);
|
|
136
|
+
if (!ids.length || !sheet) return resolvePlaceholders(area, '', cells, notes);
|
|
137
|
+
return `${quoted(sheet)}!${resolvePlaceholders(area, sheet, cells, notes).trim()}`;
|
|
138
|
+
})
|
|
139
|
+
.join(',');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The operations that write one measured workbook; formulas and charts come after every sheet's values. */
|
|
143
|
+
export function xlsxOperationsFromSheets(sheets, { captures = new Map(), notes = [] } = {}) {
|
|
144
|
+
const first = [];
|
|
145
|
+
const later = [];
|
|
146
|
+
const chartData = [];
|
|
147
|
+
const placed = new Map();
|
|
148
|
+
sheets.forEach((sheet, sheetIndex) => {
|
|
149
|
+
const name = sheet.name;
|
|
150
|
+
first.push(sheetIndex === 0 ? { op: 'rename_sheet', sheet: 'Sheet1', name } : { op: 'add_sheet', name });
|
|
151
|
+
const cells = sheet.tables.flatMap((table) => table.rows.flat());
|
|
152
|
+
const rects = [...sheet.texts, ...sheet.boxes, ...cells, ...sheet.charts, ...sheet.pictures].map(
|
|
153
|
+
(item) => item.rect
|
|
154
|
+
);
|
|
155
|
+
// The sheet ends where its content does, plus the section's own right and bottom padding: a section left at the
|
|
156
|
+
// page's width (a data sheet) gave its last column the rest of the browser window.
|
|
157
|
+
const right = Math.max(...rects.map((rect) => rect.right), 1);
|
|
158
|
+
const bottom = Math.max(...rects.map((rect) => rect.bottom), 1);
|
|
159
|
+
const xs = edges(
|
|
160
|
+
rects.flatMap((rect) => [rect.left, rect.right]),
|
|
161
|
+
right + (sheet.padRight || 0)
|
|
162
|
+
);
|
|
163
|
+
const ys = edges(
|
|
164
|
+
rects.flatMap((rect) => [rect.top, rect.bottom]),
|
|
165
|
+
bottom + (sheet.padBottom || 0)
|
|
166
|
+
);
|
|
167
|
+
const span = (rect) => {
|
|
168
|
+
const c0 = indexOf(xs, rect.left);
|
|
169
|
+
const c1 = Math.max(c0 + 1, indexOf(xs, rect.right));
|
|
170
|
+
const r0 = indexOf(ys, rect.top);
|
|
171
|
+
const r1 = Math.max(r0 + 1, indexOf(ys, rect.bottom));
|
|
172
|
+
return {
|
|
173
|
+
c0,
|
|
174
|
+
c1,
|
|
175
|
+
r0,
|
|
176
|
+
r1,
|
|
177
|
+
range: `${columnLabel(c0 + 1)}${r0 + 1}:${columnLabel(c1)}${r1}`,
|
|
178
|
+
cell: `${columnLabel(c0 + 1)}${r0 + 1}`,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
// Columns and rows at the widths and heights the browser drew.
|
|
182
|
+
for (let index = 0; index < xs.length - 1; index += 1) {
|
|
183
|
+
first.push({
|
|
184
|
+
op: 'set_column_width',
|
|
185
|
+
sheet: name,
|
|
186
|
+
column: columnLabel(index + 1),
|
|
187
|
+
width: Math.round(columnCharacters(xs[index + 1] - xs[index]) * 100) / 100,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
for (let index = 0; index < ys.length - 1; index += 1) {
|
|
191
|
+
first.push({
|
|
192
|
+
op: 'set_row_height',
|
|
193
|
+
sheet: name,
|
|
194
|
+
row: index + 1,
|
|
195
|
+
height: Math.round((ys[index + 1] - ys[index]) * PX_TO_PT * 100) / 100,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
first.push({
|
|
199
|
+
op: 'set_style',
|
|
200
|
+
sheet: name,
|
|
201
|
+
range: `A1:${columnLabel(xs.length - 1)}${ys.length - 1}`,
|
|
202
|
+
properties: { ...(sheet.font ? { fontName: sheet.font } : {}) },
|
|
203
|
+
});
|
|
204
|
+
// Boxes, outer first so a card's fill sits over its panel's: the fill on every cell, each rule on its edge.
|
|
205
|
+
const sideRanges = (s) => ({
|
|
206
|
+
top: `${columnLabel(s.c0 + 1)}${s.r0 + 1}:${columnLabel(s.c1)}${s.r0 + 1}`,
|
|
207
|
+
bottom: `${columnLabel(s.c0 + 1)}${s.r1}:${columnLabel(s.c1)}${s.r1}`,
|
|
208
|
+
left: `${columnLabel(s.c0 + 1)}${s.r0 + 1}:${columnLabel(s.c0 + 1)}${s.r1}`,
|
|
209
|
+
right: `${columnLabel(s.c1)}${s.r0 + 1}:${columnLabel(s.c1)}${s.r1}`,
|
|
210
|
+
});
|
|
211
|
+
const decorate = (item) => {
|
|
212
|
+
const s = span(item.rect);
|
|
213
|
+
if (item.fill) first.push({ op: 'set_style', sheet: name, range: s.range, properties: { fillColor: item.fill } });
|
|
214
|
+
if (item.borders) {
|
|
215
|
+
const ranges = sideRanges(s);
|
|
216
|
+
for (const [edge, border] of Object.entries(item.borders)) {
|
|
217
|
+
if (border)
|
|
218
|
+
first.push({
|
|
219
|
+
op: 'set_style',
|
|
220
|
+
sheet: name,
|
|
221
|
+
range: ranges[edge],
|
|
222
|
+
properties: { borders: { [edge]: border } },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const area = (rect) => (rect.right - rect.left) * (rect.bottom - rect.top);
|
|
228
|
+
for (const box of [...sheet.boxes].sort((a, b) => area(b.rect) - area(a.rect))) decorate(box);
|
|
229
|
+
// Texts and table cells: the value at the top-left, merged across the cells the text spans, in its own type.
|
|
230
|
+
const write = (item, { figures = false, merge = true } = {}) => {
|
|
231
|
+
const s = span(item.rect);
|
|
232
|
+
if (item.id) placed.set(item.id, { sheet: name, cell: s.cell });
|
|
233
|
+
if (item.fill || item.borders) decorate(item);
|
|
234
|
+
if (merge && (s.c1 - s.c0 > 1 || s.r1 - s.r0 > 1)) first.push({ op: 'merge_cells', sheet: name, range: s.range });
|
|
235
|
+
const content = cellContent(item, { figures });
|
|
236
|
+
if (content.formula) later.push({ op: 'set_formula', sheet: name, cell: s.cell, formula: content.formula });
|
|
237
|
+
else if (content.value !== '' && content.value !== undefined)
|
|
238
|
+
first.push({ op: 'set_range', sheet: name, range: s.cell, values: [[content.value]] });
|
|
239
|
+
// data-note: the figure's source or assumption, as the cell's note.
|
|
240
|
+
if (item.note) later.push({ op: 'add_note', sheet: name, cell: s.cell, text: item.note });
|
|
241
|
+
const wrap = item.lines > 1;
|
|
242
|
+
first.push({
|
|
243
|
+
op: 'set_style',
|
|
244
|
+
sheet: name,
|
|
245
|
+
range: merge ? s.range : s.cell,
|
|
246
|
+
properties: {
|
|
247
|
+
...(item.font ? { fontName: item.font } : {}),
|
|
248
|
+
fontSize: item.size,
|
|
249
|
+
bold: item.bold,
|
|
250
|
+
italic: item.italic,
|
|
251
|
+
color: item.color,
|
|
252
|
+
horizontalAlignment: item.align,
|
|
253
|
+
verticalAlignment: item.valign || (wrap ? 'top' : 'center'),
|
|
254
|
+
...(wrap ? { wrapText: true } : {}),
|
|
255
|
+
...(item.indent ? { indent: item.indent } : {}),
|
|
256
|
+
...(content.format ? { numberFormat: content.format } : {}),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
for (const text of sheet.texts) write(text);
|
|
261
|
+
for (const table of sheet.tables) {
|
|
262
|
+
const named = Boolean(table.name);
|
|
263
|
+
for (const row of table.rows) for (const cell of row) write(cell, { figures: !cell.header, merge: !named });
|
|
264
|
+
if (!table.rows.length) continue;
|
|
265
|
+
const corners = span(table.rect);
|
|
266
|
+
const tableRange = `${columnLabel(corners.c0 + 1)}${corners.r0 + 1}:${columnLabel(corners.c1)}${corners.r1}`;
|
|
267
|
+
if (named)
|
|
268
|
+
first.push({
|
|
269
|
+
op: 'add_table',
|
|
270
|
+
sheet: name,
|
|
271
|
+
range: tableRange,
|
|
272
|
+
name: table.name,
|
|
273
|
+
style: table.style || 'TableStyleLight1',
|
|
274
|
+
});
|
|
275
|
+
if (table.freezeHeader) later.push({ op: 'freeze_panes', sheet: name, row: corners.r0 + 2 });
|
|
276
|
+
if (table.rows[0]?.some((cell) => cell.header) && !sheet.charts.length && !sheet.boxes.length) {
|
|
277
|
+
later.push({ op: 'set_page_setup', sheet: name, printTitleRows: `${corners.r0 + 1}` });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Charts over their data: the data-range given, else the spec's labels and values in the hidden data sheet.
|
|
281
|
+
for (const chart of sheet.charts) {
|
|
282
|
+
const s = span(chart.rect);
|
|
283
|
+
let range = chart.range;
|
|
284
|
+
if (!range) {
|
|
285
|
+
const spec = chart.spec || {};
|
|
286
|
+
const series = Array.isArray(spec.series)
|
|
287
|
+
? spec.series
|
|
288
|
+
: [{ name: spec.name || chart.title || '값', values: spec.values || [] }];
|
|
289
|
+
const labels = Array.isArray(spec.labels) ? spec.labels : [];
|
|
290
|
+
const start = chartData.reduce((row, block) => row + block.length + 1, 1);
|
|
291
|
+
const block = [
|
|
292
|
+
['', ...series.map((entry) => entry.name || '')],
|
|
293
|
+
...labels.map((label, index) => [
|
|
294
|
+
String(label),
|
|
295
|
+
...series.map((entry) => Number(entry.values?.[index] ?? 0)),
|
|
296
|
+
]),
|
|
297
|
+
];
|
|
298
|
+
chartData.push(block);
|
|
299
|
+
range = `${quoted(CHART_DATA_SHEET)}!A${start}:${columnLabel(series.length + 1)}${start + block.length - 1}`;
|
|
300
|
+
}
|
|
301
|
+
later.push(chartOperation(chart, name, { cell: s.cell, toColumn: columnLabel(s.c1) }, range));
|
|
302
|
+
}
|
|
303
|
+
for (const picture of sheet.pictures) {
|
|
304
|
+
const path = captures.get(picture.capture);
|
|
305
|
+
if (!path) continue;
|
|
306
|
+
const s = span(picture.rect);
|
|
307
|
+
later.push({
|
|
308
|
+
op: 'add_image',
|
|
309
|
+
sheet: name,
|
|
310
|
+
path,
|
|
311
|
+
cell: s.cell,
|
|
312
|
+
width: (picture.rect.right - picture.rect.left) * PX_TO_PT,
|
|
313
|
+
height: (picture.rect.bottom - picture.rect.top) * PX_TO_PT,
|
|
314
|
+
altText: picture.alt || 'picture',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// The sheet as the HTML showed it: no gridlines unless asked, printed one page wide (one page for a report).
|
|
318
|
+
later.push({ op: 'set_sheet_view', sheet: name, showGridlines: Boolean(sheet.gridlines) });
|
|
319
|
+
const report = sheet.charts.length || sheet.boxes.length || sheet.texts.length;
|
|
320
|
+
later.push({
|
|
321
|
+
op: 'set_page_setup',
|
|
322
|
+
sheet: name,
|
|
323
|
+
printArea: `A1:${columnLabel(xs.length - 1)}${ys.length - 1}`,
|
|
324
|
+
orientation: sheet.orientation || ((xs.at(-1) || 0) > (ys.at(-1) || 0) ? 'landscape' : 'portrait'),
|
|
325
|
+
fitToPagesWide: 1,
|
|
326
|
+
...(report || sheet.fit === 'page' ? { fitToPagesTall: 1 } : {}),
|
|
327
|
+
});
|
|
328
|
+
if (sheet.freeze) later.push({ op: 'freeze_panes', sheet: name, row: Number(sheet.freeze) });
|
|
329
|
+
if (sheet.hidden) later.push({ op: 'set_sheet_visibility', sheet: name, visible: false });
|
|
330
|
+
});
|
|
331
|
+
if (chartData.length) {
|
|
332
|
+
first.push({ op: 'add_sheet', name: CHART_DATA_SHEET });
|
|
333
|
+
let row = 1;
|
|
334
|
+
for (const block of chartData) {
|
|
335
|
+
first.push({
|
|
336
|
+
op: 'set_range',
|
|
337
|
+
sheet: CHART_DATA_SHEET,
|
|
338
|
+
range: `A${row}:${columnLabel(block[0].length)}${row + block.length - 1}`,
|
|
339
|
+
values: block,
|
|
340
|
+
});
|
|
341
|
+
row += block.length + 1;
|
|
342
|
+
}
|
|
343
|
+
later.push({ op: 'set_sheet_visibility', sheet: CHART_DATA_SHEET, visible: false });
|
|
344
|
+
}
|
|
345
|
+
for (const operation of later) {
|
|
346
|
+
if (operation.op === 'set_formula')
|
|
347
|
+
operation.formula = resolvePlaceholders(operation.formula, operation.sheet, placed, notes);
|
|
348
|
+
if (operation.op === 'add_chart') operation.range = resolveRange(operation.range, placed, notes);
|
|
349
|
+
}
|
|
350
|
+
return [...first, ...later];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Writes the measured sheets to `output` as an .xlsx. */
|
|
354
|
+
export async function buildXlsxFromSheets(measure, output, { captures = new Map(), title = '' } = {}) {
|
|
355
|
+
if (!measure.sheets.length) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
'The HTML holds no <section data-sheet="이름">; every worksheet is one section with its sheet name.'
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
await createPortableOoxmlDocument(output, { fileKind: 'xlsx', title, sheetName: 'Sheet1' });
|
|
361
|
+
const notes = [];
|
|
362
|
+
const operations = xlsxOperationsFromSheets(measure.sheets, { captures, notes });
|
|
363
|
+
await applyPortableOoxmlBatch(output, 'xlsx', operations);
|
|
364
|
+
return { operations: operations.length, notes };
|
|
365
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// The Excel HTML route, first half: each <section data-sheet="이름"> is one worksheet the browser lays out, and this
|
|
2
|
+
// page function reads it back as what a grid can hold — every text (a title, a card's figure and label, a table cell)
|
|
3
|
+
// with its box, type, alignment and value or formula; every filled or ruled box; charts and pictures by their boxes.
|
|
4
|
+
// xlsx-html-build.mjs turns the distinct edges of those boxes into the sheet's columns and rows, so a CSS grid of
|
|
5
|
+
// cards becomes cells, merges, fills and native charts on a column grid that reproduces it.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs inside the page (page.evaluate): self-contained.
|
|
9
|
+
* @returns {{ sheets: object[], notes: string[], captures: number }}
|
|
10
|
+
*/
|
|
11
|
+
export function extractXlsxSheets() {
|
|
12
|
+
const PT = 0.75;
|
|
13
|
+
const n = (value) => parseFloat(value) || 0;
|
|
14
|
+
const notes = [];
|
|
15
|
+
let captures = 0;
|
|
16
|
+
const hex = (value) => {
|
|
17
|
+
const match = /rgba?\(([^)]+)\)/.exec(value || '');
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const parts = match[1]
|
|
20
|
+
.split(/[\s,/]+/)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map(parseFloat);
|
|
23
|
+
const alpha = parts.length > 3 ? parts[3] : 1;
|
|
24
|
+
if (!(alpha > 0.04)) return null;
|
|
25
|
+
return [0, 1, 2]
|
|
26
|
+
.map((index) =>
|
|
27
|
+
Math.round(parts[index] * alpha + 255 * (1 - alpha))
|
|
28
|
+
.toString(16)
|
|
29
|
+
.padStart(2, '0')
|
|
30
|
+
)
|
|
31
|
+
.join('')
|
|
32
|
+
.toUpperCase();
|
|
33
|
+
};
|
|
34
|
+
const GENERIC = new Set(['serif', 'sans-serif', 'monospace', 'system-ui', 'cursive', 'fantasy']);
|
|
35
|
+
const face = (cs) => {
|
|
36
|
+
const first = String(cs.fontFamily || '')
|
|
37
|
+
.split(',')[0]
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/^["']|["']$/g, '');
|
|
40
|
+
return GENERIC.has(first.toLowerCase()) ? '' : first;
|
|
41
|
+
};
|
|
42
|
+
const visible = (el) => {
|
|
43
|
+
const cs = getComputedStyle(el);
|
|
44
|
+
return cs.display !== 'none' && cs.visibility !== 'hidden';
|
|
45
|
+
};
|
|
46
|
+
const side = (cs, name) => {
|
|
47
|
+
const width = n(cs[`border${name}Width`]);
|
|
48
|
+
const style = cs[`border${name}Style`];
|
|
49
|
+
const color = hex(cs[`border${name}Color`]);
|
|
50
|
+
if (!(width > 0) || ['none', 'hidden'].includes(style) || !color) return null;
|
|
51
|
+
return {
|
|
52
|
+
style: width >= 2.5 ? 'medium' : style === 'dashed' ? 'dashed' : style === 'dotted' ? 'dotted' : 'thin',
|
|
53
|
+
color,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const bordersOf = (cs) => {
|
|
57
|
+
const sides = {
|
|
58
|
+
top: side(cs, 'Top'),
|
|
59
|
+
right: side(cs, 'Right'),
|
|
60
|
+
bottom: side(cs, 'Bottom'),
|
|
61
|
+
left: side(cs, 'Left'),
|
|
62
|
+
};
|
|
63
|
+
return Object.values(sides).some(Boolean) ? sides : null;
|
|
64
|
+
};
|
|
65
|
+
const describe = (el) =>
|
|
66
|
+
`<${el.localName}${el.className && typeof el.className === 'string' ? `.${el.className.trim().split(/\s+/).join('.')}` : ''}>`;
|
|
67
|
+
const alignOf = (cs) => {
|
|
68
|
+
const value = String(cs.textAlign || '');
|
|
69
|
+
if (value === 'center') return 'center';
|
|
70
|
+
if (value === 'right' || value === 'end') return 'right';
|
|
71
|
+
return 'left';
|
|
72
|
+
};
|
|
73
|
+
const typeOf = (cs) => {
|
|
74
|
+
const weight = Number(cs.fontWeight) || (cs.fontWeight === 'bold' ? 700 : 400);
|
|
75
|
+
return {
|
|
76
|
+
font: face(cs),
|
|
77
|
+
size: Math.round(n(cs.fontSize) * PT * 2) / 2,
|
|
78
|
+
bold: weight >= 600,
|
|
79
|
+
italic: cs.fontStyle === 'italic',
|
|
80
|
+
color: hex(cs.color) || '000000',
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const textOf = (el) =>
|
|
84
|
+
(el.innerText || el.textContent || '')
|
|
85
|
+
.replace(/[ \t]+/g, ' ')
|
|
86
|
+
.replace(/ *\n */g, '\n')
|
|
87
|
+
.trim();
|
|
88
|
+
const hasBlockChildren = (el) =>
|
|
89
|
+
[...el.children].some(
|
|
90
|
+
(child) =>
|
|
91
|
+
visible(child) &&
|
|
92
|
+
!['inline', 'inline-block', 'contents'].includes(getComputedStyle(child).display) &&
|
|
93
|
+
child.localName !== 'br'
|
|
94
|
+
);
|
|
95
|
+
const hasOwnText = (el) => [...el.childNodes].some((node) => node.nodeType === 3 && node.textContent.trim());
|
|
96
|
+
// Lines counted from the text's own height: a cell stretched by its row (a table that is a grid item stretches to
|
|
97
|
+
// the tallest item beside it) holds one line in a tall box.
|
|
98
|
+
const lines = (el, cs) => {
|
|
99
|
+
const line = cs.lineHeight === 'normal' ? n(cs.fontSize) * 1.25 : n(cs.lineHeight);
|
|
100
|
+
const range = document.createRange();
|
|
101
|
+
range.selectNodeContents(el);
|
|
102
|
+
const tops = new Set(
|
|
103
|
+
[...range.getClientRects()]
|
|
104
|
+
.filter((rect) => rect.width > 0)
|
|
105
|
+
.map((rect) => Math.round(rect.top / Math.max(1, line / 2)))
|
|
106
|
+
);
|
|
107
|
+
const height = range.getBoundingClientRect().height;
|
|
108
|
+
return Math.max(1, tops.size > 1 ? Math.round(height / Math.max(1, line)) : 1);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const sheets = [];
|
|
112
|
+
for (const section of document.querySelectorAll('section[data-sheet]')) {
|
|
113
|
+
if (!visible(section)) continue;
|
|
114
|
+
const origin = section.getBoundingClientRect();
|
|
115
|
+
const rel = (r) => ({
|
|
116
|
+
left: r.left - origin.left,
|
|
117
|
+
top: r.top - origin.top,
|
|
118
|
+
right: r.right - origin.left,
|
|
119
|
+
bottom: r.bottom - origin.top,
|
|
120
|
+
});
|
|
121
|
+
const sectionPadding = getComputedStyle(section);
|
|
122
|
+
const sheet = {
|
|
123
|
+
name: section.dataset.sheet,
|
|
124
|
+
width: origin.width,
|
|
125
|
+
height: origin.height,
|
|
126
|
+
padRight: n(sectionPadding.paddingRight),
|
|
127
|
+
padBottom: n(sectionPadding.paddingBottom),
|
|
128
|
+
gridlines: section.dataset.gridlines === 'show',
|
|
129
|
+
freeze: section.dataset.freeze || '',
|
|
130
|
+
fit: section.dataset.fit || '',
|
|
131
|
+
orientation: section.dataset.orientation || '',
|
|
132
|
+
hidden: section.dataset.hidden === 'true',
|
|
133
|
+
texts: [],
|
|
134
|
+
boxes: [],
|
|
135
|
+
tables: [],
|
|
136
|
+
charts: [],
|
|
137
|
+
pictures: [],
|
|
138
|
+
};
|
|
139
|
+
const sectionStyle = getComputedStyle(section);
|
|
140
|
+
sheet.font = face(sectionStyle);
|
|
141
|
+
const cellValue = (el) => {
|
|
142
|
+
const value = { text: textOf(el), ...(el.id ? { id: el.id } : {}) };
|
|
143
|
+
if (el.dataset.formula) value.formula = el.dataset.formula;
|
|
144
|
+
if (el.dataset.value !== undefined) value.value = el.dataset.value;
|
|
145
|
+
if (el.dataset.format) value.format = el.dataset.format;
|
|
146
|
+
if (el.dataset.note) value.note = el.dataset.note;
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
const textItem = (el, cs, extra = {}) => {
|
|
150
|
+
const rect = rel(el.getBoundingClientRect());
|
|
151
|
+
sheet.texts.push({
|
|
152
|
+
rect,
|
|
153
|
+
...cellValue(el),
|
|
154
|
+
...typeOf(cs),
|
|
155
|
+
align: alignOf(cs),
|
|
156
|
+
lines: lines(el, cs),
|
|
157
|
+
indent: alignOf(cs) === 'left' && n(cs.paddingLeft) >= 6 ? 1 : 0,
|
|
158
|
+
...extra,
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const boxItem = (el, cs) => {
|
|
162
|
+
const fill = hex(cs.backgroundColor);
|
|
163
|
+
const borders = bordersOf(cs);
|
|
164
|
+
if (fill || borders)
|
|
165
|
+
sheet.boxes.push({
|
|
166
|
+
rect: rel(el.getBoundingClientRect()),
|
|
167
|
+
...(fill ? { fill } : {}),
|
|
168
|
+
...(borders ? { borders } : {}),
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
const walk = (el) => {
|
|
172
|
+
for (const child of el.children) {
|
|
173
|
+
if (!visible(child)) continue;
|
|
174
|
+
const cs = getComputedStyle(child);
|
|
175
|
+
const tag = child.localName;
|
|
176
|
+
if (['absolute', 'fixed'].includes(cs.position))
|
|
177
|
+
notes.push(
|
|
178
|
+
`${sheet.name} ${describe(child)}: position ${cs.position} is placed where it lies; check the cell it lands on`
|
|
179
|
+
);
|
|
180
|
+
if (child.hasAttribute('data-chart')) {
|
|
181
|
+
let spec = null;
|
|
182
|
+
try {
|
|
183
|
+
spec = JSON.parse(child.dataset.chart);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
notes.push(`${sheet.name} ${describe(child)}: data-chart is not valid JSON (${error.message})`);
|
|
186
|
+
}
|
|
187
|
+
if (spec)
|
|
188
|
+
sheet.charts.push({
|
|
189
|
+
rect: rel(child.getBoundingClientRect()),
|
|
190
|
+
spec,
|
|
191
|
+
range: child.dataset.range || '',
|
|
192
|
+
title: child.dataset.title || spec.title || '',
|
|
193
|
+
font: face(cs),
|
|
194
|
+
alt: child.dataset.alt || '',
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (tag === 'img' || tag === 'svg' || tag === 'canvas') {
|
|
199
|
+
captures += 1;
|
|
200
|
+
child.setAttribute('data-xlsx-capture', String(captures));
|
|
201
|
+
sheet.pictures.push({
|
|
202
|
+
rect: rel(child.getBoundingClientRect()),
|
|
203
|
+
capture: captures,
|
|
204
|
+
alt: child.getAttribute('alt') || child.getAttribute('aria-label') || child.dataset.alt || '',
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (tag === 'table') {
|
|
209
|
+
const table = {
|
|
210
|
+
name: child.dataset.table || '',
|
|
211
|
+
style: child.dataset.tableStyle || '',
|
|
212
|
+
rect: rel(child.getBoundingClientRect()),
|
|
213
|
+
freezeHeader: child.dataset.freeze === 'header',
|
|
214
|
+
rows: [],
|
|
215
|
+
};
|
|
216
|
+
for (const row of child.rows) {
|
|
217
|
+
if (!visible(row)) continue;
|
|
218
|
+
const cells = [];
|
|
219
|
+
for (const cell of row.cells) {
|
|
220
|
+
if (!visible(cell)) continue;
|
|
221
|
+
const ccs = getComputedStyle(cell);
|
|
222
|
+
const fill = hex(ccs.backgroundColor) || hex(getComputedStyle(row).backgroundColor);
|
|
223
|
+
const rect = rel(cell.getBoundingClientRect());
|
|
224
|
+
const valign = { top: 'top', bottom: 'bottom' }[ccs.verticalAlign] || '';
|
|
225
|
+
cells.push({
|
|
226
|
+
rect,
|
|
227
|
+
...cellValue(cell),
|
|
228
|
+
...typeOf(ccs),
|
|
229
|
+
align: alignOf(ccs),
|
|
230
|
+
...(valign ? { valign } : {}),
|
|
231
|
+
lines: lines(cell, ccs),
|
|
232
|
+
header: cell.localName === 'th' || row.parentElement === child.tHead,
|
|
233
|
+
...(fill ? { fill } : {}),
|
|
234
|
+
...(bordersOf(ccs) ? { borders: bordersOf(ccs) } : {}),
|
|
235
|
+
indent: alignOf(ccs) === 'left' && n(ccs.paddingLeft) >= 6 ? 1 : 0,
|
|
236
|
+
});
|
|
237
|
+
// A spanning cell merges across the rows its box covers; an Excel table (data-table) holds no merges.
|
|
238
|
+
if ((cell.rowSpan > 1 || cell.colSpan > 1) && child.dataset.table)
|
|
239
|
+
notes.push(`${sheet.name}: a data-table cannot merge; the spanning cell keeps its first cell`);
|
|
240
|
+
}
|
|
241
|
+
table.rows.push(cells);
|
|
242
|
+
}
|
|
243
|
+
sheet.tables.push(table);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
boxItem(child, cs);
|
|
247
|
+
if (hasBlockChildren(child)) walk(child);
|
|
248
|
+
else if (textOf(child)) textItem(child, cs);
|
|
249
|
+
if (hasOwnText(child) && hasBlockChildren(child))
|
|
250
|
+
notes.push(`${sheet.name} ${describe(child)}: text beside blocks is left out; wrap it in its own element`);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
walk(section);
|
|
254
|
+
sheets.push(sheet);
|
|
255
|
+
}
|
|
256
|
+
return { sheets, notes, captures };
|
|
257
|
+
}
|