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,139 @@
|
|
|
1
|
+
// The `author` action's Excel input: lay the HTML's <section data-sheet> pages out in a local browser, read each back
|
|
2
|
+
// as a grid (xlsx-html-measure.mjs), photograph pictures and each section for the review, and write the .xlsx.
|
|
3
|
+
import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { htmlFailure, launchHtmlBrowser, photographCaptures, runInBrowser } from './html-browser.mjs';
|
|
9
|
+
import { iconTable, injectIcons } from './pptx-html-measure.mjs';
|
|
10
|
+
import { extractXlsxSheets } from './xlsx-html-measure.mjs';
|
|
11
|
+
import { drawDataCharts } from './pdf-html-charts.mjs';
|
|
12
|
+
import { buildXlsxFromSheets } from './xlsx-html-build.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs inside the page: a chart whose data-range names table cells by id (`{mh}:{m9},{wh}:{w9}`, each area one column
|
|
16
|
+
* from its header down, the first the categories) takes the labels and values those cells show, so the reference
|
|
17
|
+
* draws the chart the workbook will; a chart with values of its own, or a range not written that way, is left alone.
|
|
18
|
+
*/
|
|
19
|
+
function fillRangeCharts() {
|
|
20
|
+
for (const element of document.querySelectorAll('[data-chart][data-range]')) {
|
|
21
|
+
let spec;
|
|
22
|
+
try {
|
|
23
|
+
spec = JSON.parse(element.dataset.chart);
|
|
24
|
+
} catch {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (spec.values || spec.series) continue;
|
|
28
|
+
const columns = element.dataset.range.split(',').map((area) => {
|
|
29
|
+
const ids = [...area.matchAll(/\{([A-Za-z][\w-]*)\}/g)].map((match) => document.getElementById(match[1]));
|
|
30
|
+
const [from, to] = ids;
|
|
31
|
+
if (ids.length !== 2 || !from || !to || from.cellIndex === undefined) return null;
|
|
32
|
+
const table = from.closest('table');
|
|
33
|
+
if (!table || table !== to.closest('table') || from.cellIndex !== to.cellIndex) return null;
|
|
34
|
+
const rows = [...table.rows].slice(from.parentElement.rowIndex, to.parentElement.rowIndex + 1);
|
|
35
|
+
return rows.map((row) => (row.cells[from.cellIndex]?.innerText || '').trim());
|
|
36
|
+
});
|
|
37
|
+
if (columns.length < 2 || columns.some((column) => !column || column.length !== columns[0].length)) continue;
|
|
38
|
+
const number = (text) => Number(String(text).replace(/[^\d.-]/g, '')) || 0;
|
|
39
|
+
const [labels, ...values] = columns;
|
|
40
|
+
element.dataset.chart = JSON.stringify({
|
|
41
|
+
...spec,
|
|
42
|
+
labels: labels.slice(1),
|
|
43
|
+
...(values.length === 1
|
|
44
|
+
? { values: values[0].slice(1).map(number) }
|
|
45
|
+
: { series: values.map((column) => ({ name: column[0], values: column.slice(1).map(number) })) }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Artifacts kept beside the workbook: its HTML and the browser's picture of each sheet. */
|
|
51
|
+
export function xlsxHtmlArtifacts(target) {
|
|
52
|
+
return {
|
|
53
|
+
source: `${target}.mixdog-source.html`,
|
|
54
|
+
shot: (sheet) => `${target}.mixdog-html-sheet-${sheet}.png`,
|
|
55
|
+
compare: (page) => `${target}.mixdog-compare-page-${page}.png`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runXlsxHtmlAuthoring(html, output, { target, signal = null, timeoutMs = 90_000 }) {
|
|
60
|
+
const startedAt = performance.now();
|
|
61
|
+
const artifacts = xlsxHtmlArtifacts(target);
|
|
62
|
+
await mkdir(dirname(output), { recursive: true });
|
|
63
|
+
await rm(output, { force: true });
|
|
64
|
+
await writeFile(artifacts.source, html, 'utf8');
|
|
65
|
+
const scratch = await mkdtemp(join(tmpdir(), 'mixdog-xlsx-captures-'));
|
|
66
|
+
let browser;
|
|
67
|
+
try {
|
|
68
|
+
browser = await launchHtmlBrowser('xlsx-html-browser');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
71
|
+
return htmlFailure(error, startedAt);
|
|
72
|
+
}
|
|
73
|
+
const work = async () => {
|
|
74
|
+
const page = await browser.newPage();
|
|
75
|
+
await page.setViewport({ width: 1920, height: 1200, deviceScaleFactor: 2 });
|
|
76
|
+
await page.goto(pathToFileURL(artifacts.source).href, { waitUntil: 'load' });
|
|
77
|
+
await page.evaluate(injectIcons, iconTable().table, '0 0 24 24');
|
|
78
|
+
await page.evaluate(() => document.fonts.ready);
|
|
79
|
+
const measure = await page.evaluate(extractXlsxSheets);
|
|
80
|
+
const captures = await photographCaptures(page, 'data-xlsx-capture', measure.captures, scratch);
|
|
81
|
+
// The reference each sheet is held against: its section as the browser drew it, cut at its content (a data sheet's
|
|
82
|
+
// section runs the window's width), with its charts drawn — a chart over its cells (data-range="{a}:{b},…") from
|
|
83
|
+
// the text those cells show.
|
|
84
|
+
await page.evaluate(fillRangeCharts);
|
|
85
|
+
await page.evaluate(drawDataCharts);
|
|
86
|
+
const extents = await page.evaluate(() =>
|
|
87
|
+
[...document.querySelectorAll('section[data-sheet]')].map((section) => {
|
|
88
|
+
const box = section.getBoundingClientRect();
|
|
89
|
+
const cs = getComputedStyle(section);
|
|
90
|
+
let right = box.left;
|
|
91
|
+
let bottom = box.top;
|
|
92
|
+
for (const el of section.querySelectorAll('*')) {
|
|
93
|
+
const r = el.getBoundingClientRect();
|
|
94
|
+
if (!r.width || !r.height) continue;
|
|
95
|
+
right = Math.max(right, r.right);
|
|
96
|
+
bottom = Math.max(bottom, r.bottom);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
x: box.left + window.scrollX,
|
|
100
|
+
y: box.top + window.scrollY,
|
|
101
|
+
width: Math.min(box.width, right - box.left + (parseFloat(cs.paddingRight) || 0)),
|
|
102
|
+
height: Math.min(box.height, bottom - box.top + (parseFloat(cs.paddingBottom) || 0)),
|
|
103
|
+
};
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
const htmlPages = [];
|
|
107
|
+
for (const [index, clip] of extents.entries()) {
|
|
108
|
+
const path = artifacts.shot(index + 1);
|
|
109
|
+
await page.screenshot({ path, clip, captureBeyondViewport: true });
|
|
110
|
+
htmlPages.push({ page: index + 1, path });
|
|
111
|
+
}
|
|
112
|
+
return { measure, captures, htmlPages, title: await page.title() };
|
|
113
|
+
};
|
|
114
|
+
let laid;
|
|
115
|
+
try {
|
|
116
|
+
laid = await runInBrowser(browser, { signal, timeoutMs }, work);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (signal?.aborted) throw error;
|
|
119
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
120
|
+
return htmlFailure(error, startedAt);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const built = await buildXlsxFromSheets(laid.measure, output, { captures: laid.captures, title: laid.title });
|
|
124
|
+
laid.measure.notes.push(...built.notes);
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
output,
|
|
128
|
+
bytes: (await stat(output)).size,
|
|
129
|
+
htmlSource: artifacts.source,
|
|
130
|
+
htmlPages: laid.htmlPages,
|
|
131
|
+
logs: laid.measure.notes.map((text) => ({ level: 'warn', text })),
|
|
132
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return htmlFailure(error, startedAt);
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -99,6 +99,7 @@ export const CATALOG = {
|
|
|
99
99
|
'set_paragraph_format',
|
|
100
100
|
'set_font',
|
|
101
101
|
'set_document_font',
|
|
102
|
+
'define_styles',
|
|
102
103
|
'add_image',
|
|
103
104
|
// Drawn as a picture before either backend sees it (office-actions-batch withDocumentCharts).
|
|
104
105
|
'add_chart',
|
|
@@ -169,6 +170,8 @@ export const CATALOG = {
|
|
|
169
170
|
'headerBold',
|
|
170
171
|
'headerRows',
|
|
171
172
|
'keepWithNext',
|
|
173
|
+
'rowStyles',
|
|
174
|
+
'totalRow',
|
|
172
175
|
],
|
|
173
176
|
tableStyle: ['style', 'alignment', 'columnAlignments', 'columnWidths', 'borders', 'shading'],
|
|
174
177
|
tableCell: [
|
|
@@ -317,6 +320,11 @@ export const CATALOG = {
|
|
|
317
320
|
'valueNumberFormat',
|
|
318
321
|
'dataLabelPosition',
|
|
319
322
|
'dataLabelColor',
|
|
323
|
+
'highlight',
|
|
324
|
+
'mutedColor',
|
|
325
|
+
'gridlines',
|
|
326
|
+
'valueAxis',
|
|
327
|
+
'fontName',
|
|
320
328
|
],
|
|
321
329
|
table: ['name', 'style'],
|
|
322
330
|
pivot: ['source', 'destination', 'name', 'rows', 'columns', 'values'],
|
|
@@ -68,7 +68,7 @@ export const FORMAT_SIGNATURES = {
|
|
|
68
68
|
add_table: signature(['values'], ['paragraph', 'rows', 'columns', 'properties'], {
|
|
69
69
|
propertySets: ['table'],
|
|
70
70
|
notes:
|
|
71
|
-
'properties.alignment places the table on the page (left, center, right); properties.columnAlignments sets the text of each column (one of left, center, right, justify per column); without it a column of figures (184,200, 2.1%, 2.6억 원) sets right and the rest left. Without style, borders, or shading the table takes a bold header row with a rule under it and hairlines between rows; headerBold:false keeps the header plain. properties.headerRows (default 1) is how many rows the header takes: a two-level header, its group label merged across the columns it spans (merge_table_cells), is headerRows:2, and every header row is set bold on its bottom edge and repeats on a continuation page.',
|
|
71
|
+
'properties.alignment places the table on the page (left, center, right); properties.columnAlignments sets the text of each column (one of left, center, right, justify per column); without it a column of figures (184,200, 2.1%, 2.6억 원) sets right and the rest left. Without style, borders, or shading the table takes a bold header row with a rule under it and hairlines between rows; headerBold:false keeps the header plain. properties.headerRows (default 1) is how many rows the header takes: a two-level header, its group label merged across the columns it spans (merge_table_cells), is headerRows:2, and every header row is set bold on its bottom edge and repeats on a continuation page. properties.totalRow:true sets the last row as the total (bold over a rule). properties.rowStyles gives rows their own type, one entry per row from the first (null keeps the table\'s): [{ fontSize: 22, color: "1F5E4B" }, { fontSize: 9, color: "6B7280", bold: false }] is a stat strip — figures over their labels in one table.',
|
|
72
72
|
}),
|
|
73
73
|
set_table_style: signature(['table', 'properties'], [], {
|
|
74
74
|
propertySets: ['tableStyle'],
|
|
@@ -101,6 +101,10 @@ export const FORMAT_SIGNATURES = {
|
|
|
101
101
|
notes:
|
|
102
102
|
"The document's own face, size, and ink — what every paragraph without its own reads, and what a paragraph added later in Word or by append_text starts from. properties: name, nameEastAsia, size, color.",
|
|
103
103
|
}),
|
|
104
|
+
define_styles: signature(['styles'], [], {
|
|
105
|
+
notes:
|
|
106
|
+
'styles maps a style name to its fields — { "Title": { size: 24, bold: true, spacingAfter: 8 }, "Heading 1": { size: 15, bold: true, color: "1F5E4B", spacingBefore: 18, spacingAfter: 6, keepWithNext: true }, "Normal": { lineSpacing: 18, spacingAfter: 8, alignment: "left" }, "Caption": { size: 9, color: "6B7280" } } — fields: name, nameEastAsia, size, bold, italic, color, alignment, spacingBefore, spacingAfter, lineSpacing, lineSpacingRule, keepWithNext, keepTogether. Built-in styles are reached by their English name in any Word language. Define them before the paragraphs: append_text style:"Heading 1" then takes the definition with no properties of its own, and a paragraph the reader adds in Word under that style matches.',
|
|
107
|
+
}),
|
|
104
108
|
set_font: signature(['find', 'properties'], [], {
|
|
105
109
|
propertySets: ['font'],
|
|
106
110
|
notes:
|
|
@@ -179,7 +183,7 @@ export const FORMAT_SIGNATURES = {
|
|
|
179
183
|
['section', 'kind', 'variant', 'prefix', 'separator', 'includeTotal', 'alignment'],
|
|
180
184
|
{
|
|
181
185
|
notes:
|
|
182
|
-
"Writes into the footer unless kind:'header' asks otherwise; variant picks default, first, or even, as for set_header_footer. The number alone, centred, on both backends; prefix ('Page') and includeTotal:true
|
|
186
|
+
"Writes into the footer unless kind:'header' asks otherwise; variant picks default, first, or even, as for set_header_footer. The number alone, centred, on both backends; prefix ('Page') adds to it, and a separator (' / ') or includeTotal:true adds the total ('3 / 12'; includeTotal:false keeps the number alone). A cover without the running line: set_header_footer variant:'first' text:'' (and the same for the footer) after the default ones.",
|
|
183
187
|
}
|
|
184
188
|
),
|
|
185
189
|
insert_break: signature([], ['paragraph', 'kind'], {
|
|
@@ -239,7 +243,11 @@ export const FORMAT_SIGNATURES = {
|
|
|
239
243
|
}
|
|
240
244
|
),
|
|
241
245
|
set_cell: signature(['cell', 'value'], ['sheet']),
|
|
242
|
-
set_formula: signature(['
|
|
246
|
+
set_formula: signature(['formula'], ['sheet'], {
|
|
247
|
+
oneOf: [['cell'], ['range']],
|
|
248
|
+
notes:
|
|
249
|
+
'cell writes one formula. range fills it as Excel does: the formula is written for the top-left cell and every other cell takes it moved — relative references follow the cell, $-pinned ones stay, a table column (Ops[처리량 (건)]) stays — so range:"C6:C9" with =SUMIFS(Ops[처리량 (건)],Ops[월],B6) writes B7, B8, B9 into the rows below.',
|
|
250
|
+
}),
|
|
243
251
|
set_range: signature(['range', 'values'], ['sheet']),
|
|
244
252
|
append_row: signature(['values'], ['sheet'], {
|
|
245
253
|
notes:
|
|
@@ -310,11 +318,16 @@ export const FORMAT_SIGNATURES = {
|
|
|
310
318
|
'dataLabelPosition',
|
|
311
319
|
'dataLabelColor',
|
|
312
320
|
'plotBy',
|
|
321
|
+
'highlight',
|
|
322
|
+
'mutedColor',
|
|
323
|
+
'gridlines',
|
|
324
|
+
'valueAxis',
|
|
325
|
+
'fontName',
|
|
313
326
|
],
|
|
314
327
|
{
|
|
315
328
|
propertySets: ['chart'],
|
|
316
329
|
notes:
|
|
317
|
-
"plotBy:'rows' reads one bounded range the other way — the first row supplies the categories and every other row is a series named by its first cell — for a sheet that grows a column per period. The first source column supplies categories; remaining columns become series. A series that is not beside its categories joins by comma the way Excel reads it (range:'A7:A12,D7:D12', same rows in every area). A source on another sheet names it on every area (range:\"'Calc'!A1:A7,'Calc'!D1:D7\"); sheet is where the frame stands. cell (H2) places the frame's top-left corner on the grid; left/top are points and win when both are given; width/height are points (420 × 260 at F5 reaches about N22), and the print area has to reach past the frame. toColumn (F) with cell ends the frame at that column's right edge in place of width, so a chart spans a table exactly: a column's points depend on the workbook's font (a Korean Excel's is wider), which a width cannot know.",
|
|
330
|
+
"highlight (a 0-based index or a category) sets one bar or slice-free point in the series colour and the rest in mutedColor (C9CED6), so the chart says which one it is about — one series only. gridlines:false and valueAxis:false quiet the plot when every bar carries its value (showValues). fontName sets every text in the chart in the sheet's face (a Korean workbook's Malgun Gothic), where it otherwise takes the workbook default. plotBy:'rows' reads one bounded range the other way — the first row supplies the categories and every other row is a series named by its first cell — for a sheet that grows a column per period. The first source column supplies categories; remaining columns become series. A series that is not beside its categories joins by comma the way Excel reads it (range:'A7:A12,D7:D12', same rows in every area). A source on another sheet names it on every area (range:\"'Calc'!A1:A7,'Calc'!D1:D7\"); sheet is where the frame stands. cell (H2) places the frame's top-left corner on the grid; left/top are points and win when both are given; width/height are points (420 × 260 at F5 reaches about N22), and the print area has to reach past the frame. toColumn (F) with cell ends the frame at that column's right edge in place of width, so a chart spans a table exactly: a column's points depend on the workbook's font (a Korean Excel's is wider), which a width cannot know.",
|
|
318
331
|
}
|
|
319
332
|
),
|
|
320
333
|
add_conditional_format: signature(
|
|
@@ -330,8 +343,7 @@ export const FORMAT_SIGNATURES = {
|
|
|
330
343
|
['range', 'formula1'],
|
|
331
344
|
['sheet', 'type', 'operator', 'formula2', 'inputMessage', 'errorMessage'],
|
|
332
345
|
{
|
|
333
|
-
notes:
|
|
334
|
-
`type: ${XLSX_VALIDATION_TYPES.join(', ')}. Without type, choices ("a,b,c" or $A$1:$A$9) mean list; other formulas mean custom. Numeric/date/time/textLength rules take operator: ${XLSX_VALIDATION_OPERATORS.join(', ')} (default between). between/notBetween require formula2; other operators use formula1. Example: type:"whole", operator:"greaterThanOrEqual", formula1:"0". Under protect_sheet unlock entry cells with set_style properties:{ locked:false }.`,
|
|
346
|
+
notes: `type: ${XLSX_VALIDATION_TYPES.join(', ')}. Without type, choices ("a,b,c" or $A$1:$A$9) mean list; other formulas mean custom. Numeric/date/time/textLength rules take operator: ${XLSX_VALIDATION_OPERATORS.join(', ')} (default between). between/notBetween require formula2; other operators use formula1. Example: type:"whole", operator:"greaterThanOrEqual", formula1:"0". Under protect_sheet unlock entry cells with set_style properties:{ locked:false }.`,
|
|
335
347
|
}
|
|
336
348
|
),
|
|
337
349
|
freeze_panes: signature([], ['sheet', 'row', 'column'], {
|
|
@@ -4,6 +4,7 @@ import { FIELD_ALIASES, OPERATION_ALIASES, PROPERTY_ALIASES } from './capabiliti
|
|
|
4
4
|
import { BACKENDS, CATALOG, COMMON, VIRTUAL_OPERATIONS } from './capabilities-catalog.mjs';
|
|
5
5
|
import { COMMON_SIGNATURES, FORMAT_SIGNATURES, signature } from './capabilities-signatures.mjs';
|
|
6
6
|
import { normalizeXlsxValidation } from './portable/xlsx-contract.mjs';
|
|
7
|
+
import { docxStyleDefinitions } from './portable/portable-docx-operations.mjs';
|
|
7
8
|
|
|
8
9
|
export { OFFICE_ACTIONS } from './capabilities-catalog.mjs';
|
|
9
10
|
|
|
@@ -149,7 +150,10 @@ function operationProperties(catalog, signatureValue, backend) {
|
|
|
149
150
|
.map((name) => [
|
|
150
151
|
name,
|
|
151
152
|
catalog.properties[name].filter(
|
|
152
|
-
(field) =>
|
|
153
|
+
(field) =>
|
|
154
|
+
!backend ||
|
|
155
|
+
!signatureValue.propertyBackends[field] ||
|
|
156
|
+
signatureValue.propertyBackends[field].includes(backend)
|
|
153
157
|
),
|
|
154
158
|
])
|
|
155
159
|
);
|
|
@@ -271,6 +275,14 @@ function applyFieldAliases(format, name, operation) {
|
|
|
271
275
|
// handed to a single-cell operation is one, and says which operation writes it.
|
|
272
276
|
function singleCellRangeFault(batch, name, operation, index, allowed) {
|
|
273
277
|
const { format, backend } = batch;
|
|
278
|
+
// set_formula takes a range to fill; one cell named that way is still the cell it is.
|
|
279
|
+
if (format === 'xlsx' && name === 'set_formula' && operation.cell === undefined) {
|
|
280
|
+
if (SINGLE_CELL_REFERENCE.test(String(operation.range ?? '').trim())) {
|
|
281
|
+
operation.cell = operation.range;
|
|
282
|
+
delete operation.range;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
274
286
|
if (format !== 'xlsx' || !allowed.has('cell') || allowed.has('range')) return null;
|
|
275
287
|
if (operation.range === undefined || operation.cell !== undefined) return null;
|
|
276
288
|
if (SINGLE_CELL_REFERENCE.test(String(operation.range).trim())) {
|
|
@@ -404,6 +416,18 @@ function unknownPropertiesFault(batch, name, operation, index, allowedProperties
|
|
|
404
416
|
return `${format.toUpperCase()} operation "${name}" at operation ${index + 1} has unknown properties: ${unknownProperties.join(', ')}.${didYouMean(corrections)} ${name} properties: ${[...allowedProperties].join(', ')}. ${describeHint(format, backend, name)}`;
|
|
405
417
|
}
|
|
406
418
|
|
|
419
|
+
// define_styles carries its fields one level down, per style; both backends read them only after this check, so a
|
|
420
|
+
// misspelt field is refused here on either backend instead of being dropped by one of them.
|
|
421
|
+
function docxStyleDefinitionFault(format, name, operation, index) {
|
|
422
|
+
if (format !== 'docx' || name !== 'define_styles' || operation.styles === undefined) return null;
|
|
423
|
+
try {
|
|
424
|
+
docxStyleDefinitions(operation);
|
|
425
|
+
return null;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return `DOCX operation "define_styles" at operation ${index + 1}: ${error.message}`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
407
431
|
// A Word table has two alignments that read alike: `alignment` places the
|
|
408
432
|
// table on the page and `columnAlignments` sets the text of each column.
|
|
409
433
|
// A list written into the first used to be serialized verbatim into the
|
|
@@ -490,6 +514,7 @@ function operationContractFaults(batch, operation, index) {
|
|
|
490
514
|
unknownFieldsFault(batch, name, operation, index, { allowed, propertyKeys, missing }),
|
|
491
515
|
unknownPropertiesFault(batch, name, operation, index, propertyKeys),
|
|
492
516
|
...docxTableAlignmentFaults(format, name, operation, index),
|
|
517
|
+
docxStyleDefinitionFault(format, name, operation, index),
|
|
493
518
|
requiredInputFault(batch, name, operation, index, signatureValue, stableTargets),
|
|
494
519
|
].filter(Boolean);
|
|
495
520
|
if (!faults.length && format === 'xlsx' && name === 'add_validation') {
|
|
@@ -370,11 +370,23 @@ function Snapshot-Word($doc, $payload) {
|
|
|
370
370
|
# The cell's accepted view and its struck-through words, as for a
|
|
371
371
|
# paragraph, so a redline in a table reads the same way.
|
|
372
372
|
try {
|
|
373
|
-
$
|
|
373
|
+
$cellRange = $table.Cell($r, $c).Range
|
|
374
|
+
$accepted = Word-AcceptedText $doc $cellRange $context
|
|
375
|
+
# The cell's type, as the portable reader states it: a stat strip's label set at the figure's size.
|
|
376
|
+
$fontSize = 0
|
|
377
|
+
$fontBold = $false
|
|
378
|
+
try {
|
|
379
|
+
$measured = [double]$cellRange.Font.Size
|
|
380
|
+
if ($measured -gt 0 -and $measured -lt 1000) { $fontSize = $measured }
|
|
381
|
+
$fontBold = [int]$cellRange.Font.Bold -eq -1
|
|
382
|
+
}
|
|
383
|
+
catch {}
|
|
374
384
|
$cells += [ordered]@{
|
|
375
385
|
text = ([string]$accepted.text).TrimEnd("`r", "`a")
|
|
376
386
|
deletedText = ([string]$accepted.deleted).TrimEnd("`r", "`a")
|
|
377
387
|
tracked = [bool]$accepted.tracked
|
|
388
|
+
size = $fontSize
|
|
389
|
+
bold = $fontBold
|
|
378
390
|
}
|
|
379
391
|
}
|
|
380
392
|
catch { $cells += $null }
|
|
@@ -387,6 +399,9 @@ function Snapshot-Word($doc, $payload) {
|
|
|
387
399
|
for ($c = 1; $c -le @($rows[$r - 1]).Count; $c++) {
|
|
388
400
|
$record = @($rows[$r - 1])[$c - 1]
|
|
389
401
|
$cell = [ordered]@{ path = "/body/tbl[$i]/row[$r]/cell[$c]"; index = $c; text = $(if ($null -eq $record) { $null } else { $record.text }) }
|
|
402
|
+
if ($null -ne $record -and ($record.size -gt 0 -or $record.bold)) {
|
|
403
|
+
$cell.font = [ordered]@{ size = $record.size; bold = [bool]$record.bold }
|
|
404
|
+
}
|
|
390
405
|
if ($null -ne $record -and $record.tracked) { $cell.tracked = $true }
|
|
391
406
|
if ($null -ne $record -and $record.deletedText.Length -gt 0) { $cell.deletedText = $record.deletedText }
|
|
392
407
|
$cells += $cell
|
|
@@ -673,10 +688,25 @@ function Excel-FormulaPrecedents([string]$formula, [string]$currentSheet) {
|
|
|
673
688
|
return $output
|
|
674
689
|
}
|
|
675
690
|
|
|
691
|
+
# A cell's own horizontal alignment in the portable reader's words; $null for Excel's General (the cell states none),
|
|
692
|
+
# which the portable reader leaves out too.
|
|
693
|
+
function Excel-HorizontalAlignmentName($value) {
|
|
694
|
+
switch ([int]$value) {
|
|
695
|
+
-4131 { return 'left' }
|
|
696
|
+
-4108 { return 'center' }
|
|
697
|
+
-4152 { return 'right' }
|
|
698
|
+
-4130 { return 'justify' }
|
|
699
|
+
7 { return 'centerContinuous' }
|
|
700
|
+
-4117 { return 'distributed' }
|
|
701
|
+
5 { return 'fill' }
|
|
702
|
+
default { return $null }
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
676
706
|
# One cell's style as the snapshot reports it.
|
|
677
707
|
function Excel-CellStyle($cell) {
|
|
678
708
|
$font = $cell.Font
|
|
679
|
-
|
|
709
|
+
$style = [ordered]@{
|
|
680
710
|
fontName = [string]$font.Name
|
|
681
711
|
fontSize = [double]$font.Size
|
|
682
712
|
bold = [bool]$font.Bold
|
|
@@ -685,6 +715,9 @@ function Excel-CellStyle($cell) {
|
|
|
685
715
|
fillColor = [double]$cell.Interior.Color
|
|
686
716
|
numberFormat = Excel-EnglishNumberFormat $cell
|
|
687
717
|
}
|
|
718
|
+
$alignment = $(try { Excel-HorizontalAlignmentName $cell.HorizontalAlignment } catch { $null })
|
|
719
|
+
if ($alignment) { $style.horizontalAlignment = $alignment }
|
|
720
|
+
return $style
|
|
688
721
|
}
|
|
689
722
|
|
|
690
723
|
# The style every cell of a block shares, or $null when they differ: Excel answers a mixed range's property with
|
|
@@ -706,7 +739,9 @@ function Excel-SharedCellStyle($range) {
|
|
|
706
739
|
if ($null -eq $color -or $color -is [System.DBNull]) { return $null }
|
|
707
740
|
$italic = $font.Italic
|
|
708
741
|
if ($null -eq $italic -or $italic -is [System.DBNull]) { return $null }
|
|
709
|
-
|
|
742
|
+
$horizontal = $range.HorizontalAlignment
|
|
743
|
+
if ($null -eq $horizontal -or $horizontal -is [System.DBNull]) { return $null }
|
|
744
|
+
$style = [ordered]@{
|
|
710
745
|
fontName = [string]$name
|
|
711
746
|
fontSize = [double]$size
|
|
712
747
|
bold = [bool]$bold
|
|
@@ -715,6 +750,9 @@ function Excel-SharedCellStyle($range) {
|
|
|
715
750
|
fillColor = [double]$fill
|
|
716
751
|
numberFormat = Excel-EnglishNumberFormat $range ([string]$format)
|
|
717
752
|
}
|
|
753
|
+
$alignment = Excel-HorizontalAlignmentName $horizontal
|
|
754
|
+
if ($alignment) { $style.horizontalAlignment = $alignment }
|
|
755
|
+
return $style
|
|
718
756
|
}
|
|
719
757
|
|
|
720
758
|
# The styles of one column's populated cells ($rows: their sheet rows, ascending) into $styles by address. Asking
|
|
@@ -1289,16 +1327,7 @@ function Snapshot-ExcelPage($book, $payload) {
|
|
|
1289
1327
|
}
|
|
1290
1328
|
}
|
|
1291
1329
|
if ($detailed -and $payload.includeStyles) {
|
|
1292
|
-
$
|
|
1293
|
-
$entry.style = [ordered]@{
|
|
1294
|
-
fontName = [string]$cell.Font.Name
|
|
1295
|
-
fontSize = [double]$cell.Font.Size
|
|
1296
|
-
bold = [bool]$cell.Font.Bold
|
|
1297
|
-
italic = [bool]$cell.Font.Italic
|
|
1298
|
-
color = [double]$cell.Font.Color
|
|
1299
|
-
fillColor = [double]$cell.Interior.Color
|
|
1300
|
-
numberFormat = Excel-EnglishNumberFormat $cell
|
|
1301
|
-
}
|
|
1330
|
+
$entry.style = Excel-CellStyle $range.Cells.Item($r, $c)
|
|
1302
1331
|
}
|
|
1303
1332
|
if ($detailed) { $cells.Add($entry) }
|
|
1304
1333
|
}
|
|
@@ -2986,6 +3015,32 @@ function Invoke-WordOperation($doc, $op) {
|
|
|
2986
3015
|
if ($rows -gt 1 -and $props.repeatHeader -ne $false) {
|
|
2987
3016
|
for ($row = 1; $row -le $headerRows; $row++) { try { $table.Rows.Item($row).HeadingFormat = $true } catch {} }
|
|
2988
3017
|
}
|
|
3018
|
+
# The last row is the total when totalRow is set: bold over a rule, as the portable writer draws it.
|
|
3019
|
+
if ($props.totalRow -eq $true -and $rows -gt 1) {
|
|
3020
|
+
$total = $table.Rows.Item($rows)
|
|
3021
|
+
$total.Range.Font.Bold = -1
|
|
3022
|
+
$rule = $total.Borders.Item(-1) # wdBorderTop
|
|
3023
|
+
$rule.LineStyle = 1
|
|
3024
|
+
$rule.LineWidth = 8 # wdLineWidth100pt, the portable writer's w:sz 8
|
|
3025
|
+
$rule.Color = Color-Value '374151'
|
|
3026
|
+
}
|
|
3027
|
+
# One row's own type (rowStyles[row]: fontSize, color, bold) over the table's, with the row's exact line
|
|
3028
|
+
# pitch following its size: a stat strip's 22 pt figures over its 9 pt labels in one table.
|
|
3029
|
+
if ($props.rowStyles) {
|
|
3030
|
+
$rowStyles = @($props.rowStyles)
|
|
3031
|
+
for ($row = 1; $row -le [Math]::Min($rows, $rowStyles.Count); $row++) {
|
|
3032
|
+
$style = $rowStyles[$row - 1]
|
|
3033
|
+
if ($null -eq $style) { continue }
|
|
3034
|
+
$rowRange = $table.Rows.Item($row).Range
|
|
3035
|
+
if ($style.fontSize) {
|
|
3036
|
+
$rowRange.Font.Size = [single]$style.fontSize
|
|
3037
|
+
$rowRange.ParagraphFormat.LineSpacingRule = 4
|
|
3038
|
+
$rowRange.ParagraphFormat.LineSpacing = [single]$style.fontSize * 1.3
|
|
3039
|
+
}
|
|
3040
|
+
if ($style.color) { $rowRange.Font.Color = Color-Value ([string]$style.color) }
|
|
3041
|
+
if ($null -ne $style.bold) { $rowRange.Font.Bold = $(if ($style.bold) { -1 } else { 0 }) }
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2989
3044
|
$added = [ordered]@{ op = 'add_table'; changed = $true; table = [int]$table.Index; rows = $rows; columns = $columns }
|
|
2990
3045
|
if ($styleNotFound.Count) { $added.styleNotFound = $styleNotFound -join ', ' }
|
|
2991
3046
|
return $added
|
|
@@ -3218,6 +3273,27 @@ function Invoke-WordOperation($doc, $op) {
|
|
|
3218
3273
|
if ($props.color) { $normal.Font.Color = Color-Value ([string]$props.color) }
|
|
3219
3274
|
return [ordered]@{ op = 'set_document_font'; changed = $true; name = [string]$normal.Font.Name; nameEastAsia = [string]$normal.Font.NameFarEast; size = [double]$normal.Font.Size }
|
|
3220
3275
|
}
|
|
3276
|
+
'define_styles' {
|
|
3277
|
+
# Word's own styles defined once, as the portable writer patches styles.xml: built-in styles by their id
|
|
3278
|
+
# (Word-StyleValue), so a Korean Word's "제목 1" is reached as "Heading 1".
|
|
3279
|
+
if (-not $op.styles) { throw 'define_styles needs styles: { "<style name>": { fields } }' }
|
|
3280
|
+
$defined = @()
|
|
3281
|
+
$notFound = @()
|
|
3282
|
+
foreach ($entry in $op.styles.PSObject.Properties) {
|
|
3283
|
+
$style = $null
|
|
3284
|
+
try { $style = $doc.Styles.Item((Word-StyleValue ([string]$entry.Name))) } catch { $style = $null }
|
|
3285
|
+
if ($null -eq $style) { $notFound += [string]$entry.Name; continue }
|
|
3286
|
+
$fields = $entry.Value
|
|
3287
|
+
Set-WordRunFormat $style $fields
|
|
3288
|
+
$format = $style.ParagraphFormat
|
|
3289
|
+
if ($fields.alignment) { $format.Alignment = Word-ParagraphAlignment $fields.alignment }
|
|
3290
|
+
Set-WordParagraphFlow $format $fields
|
|
3291
|
+
$defined += [string]$entry.Name
|
|
3292
|
+
}
|
|
3293
|
+
$result = [ordered]@{ op = 'define_styles'; changed = $defined.Count -gt 0; styles = $defined }
|
|
3294
|
+
if ($notFound.Count) { $result.styleNotFound = $notFound }
|
|
3295
|
+
return $result
|
|
3296
|
+
}
|
|
3221
3297
|
'set_font' {
|
|
3222
3298
|
$range = $doc.Content.Duplicate
|
|
3223
3299
|
if (-not $op.find) { throw 'set_font requires non-empty find' }
|
|
@@ -3455,16 +3531,18 @@ function Invoke-WordOperation($doc, $op) {
|
|
|
3455
3531
|
if ($said) { $footer.Range.InsertParagraphAfter() }
|
|
3456
3532
|
$range = $footer.Range.Paragraphs.Last.Range
|
|
3457
3533
|
if ($said) { $null = $range.MoveEnd(1, -1) } else { $range = $footer.Range }
|
|
3458
|
-
# The same treatment as the portable writer: the number alone, a prefix
|
|
3459
|
-
#
|
|
3534
|
+
# The same treatment as the portable writer: the number alone, a prefix only when asked for, and the
|
|
3535
|
+
# total with includeTotal:true or a separator ("3 / 12"; includeTotal:false keeps the number alone).
|
|
3460
3536
|
$range.Text = $(if ($op.prefix) { ([string]$op.prefix) + ' ' } else { '' })
|
|
3461
3537
|
$range = $footer.Range
|
|
3462
3538
|
$range.Collapse(0)
|
|
3463
3539
|
$null = $footer.Range.Fields.Add($range, -1, 'PAGE', $true)
|
|
3464
|
-
|
|
3540
|
+
$separatorText = ([string]$op.separator).Trim()
|
|
3541
|
+
$includeTotal = ([bool]$op.includeTotal) -or ($op.includeTotal -ne $false -and $separatorText)
|
|
3542
|
+
if ($includeTotal) {
|
|
3465
3543
|
$range = $footer.Range
|
|
3466
3544
|
$range.Collapse(0)
|
|
3467
|
-
$range.InsertAfter(' ' + $(if ($
|
|
3545
|
+
$range.InsertAfter(' ' + $(if ($separatorText) { $separatorText } else { '/' }) + ' ')
|
|
3468
3546
|
$range = $footer.Range
|
|
3469
3547
|
$range.Collapse(0)
|
|
3470
3548
|
$null = $footer.Range.Fields.Add($range, -1, 'NUMPAGES', $true)
|
|
@@ -4384,6 +4462,16 @@ function Apply-ExcelOperation($book, $op) {
|
|
|
4384
4462
|
} 'Excel chart series by rows'
|
|
4385
4463
|
}
|
|
4386
4464
|
}
|
|
4465
|
+
# Every text in the chart in the sheet's face, where Excel otherwise sets the workbook default. Set before the
|
|
4466
|
+
# title's face: the chart area's font, set after it, put the title back in Excel's regular grey.
|
|
4467
|
+
if ($op.fontName) {
|
|
4468
|
+
try {
|
|
4469
|
+
$font = $chart.ChartArea.Format.TextFrame2.TextRange.Font
|
|
4470
|
+
$font.Name = [string]$op.fontName
|
|
4471
|
+
$font.NameFarEast = [string]$op.fontName
|
|
4472
|
+
}
|
|
4473
|
+
catch {}
|
|
4474
|
+
}
|
|
4387
4475
|
if ($op.title) {
|
|
4388
4476
|
$null = Invoke-ExcelComRetry {
|
|
4389
4477
|
$chart.HasTitle = $true
|
|
@@ -4496,6 +4584,34 @@ function Apply-ExcelOperation($book, $op) {
|
|
|
4496
4584
|
else {
|
|
4497
4585
|
0
|
|
4498
4586
|
}
|
|
4587
|
+
# The quiet plot and the one lit bar, as the portable writer draws them: gridlines:false, valueAxis:false,
|
|
4588
|
+
# highlight (a 0-based index or a category's text) in the series colour with the rest in mutedColor.
|
|
4589
|
+
if ($op.gridlines -eq $false) { try { $chart.Axes(2, 1).HasMajorGridlines = $false } catch {} }
|
|
4590
|
+
if ($op.valueAxis -eq $false) { try { $chart.Axes(2, 1).Delete() } catch {} }
|
|
4591
|
+
if ($null -ne $op.highlight -and [string]$op.highlight -ne '' -and $seriesCount -eq 1 -and -not $slices) {
|
|
4592
|
+
$series = $chart.SeriesCollection().Item(1)
|
|
4593
|
+
$lit = -1
|
|
4594
|
+
if ($op.highlight -is [ValueType]) { $lit = [int]$op.highlight }
|
|
4595
|
+
else {
|
|
4596
|
+
$names = @(@($series.XValues) | ForEach-Object { [string]$_ })
|
|
4597
|
+
$lit = [Array]::IndexOf($names, [string]$op.highlight)
|
|
4598
|
+
}
|
|
4599
|
+
if ($lit -ge 0 -and $lit -lt $pointCount) {
|
|
4600
|
+
$accent = Color-Value $(if ($palette.Count) { [string]$palette[0] } else { '2F6DB5' })
|
|
4601
|
+
$muted = Color-Value $(if ($op.mutedColor) { [string]$op.mutedColor } else { 'C9CED6' })
|
|
4602
|
+
for ($pointIndex = 1; $pointIndex -le $pointCount; $pointIndex++) {
|
|
4603
|
+
try {
|
|
4604
|
+
$point = $series.Points($pointIndex)
|
|
4605
|
+
$pointColor = $(if ($pointIndex -eq $lit + 1) { $accent } else { $muted })
|
|
4606
|
+
$point.Format.Fill.Solid()
|
|
4607
|
+
$point.Format.Fill.ForeColor.RGB = $pointColor
|
|
4608
|
+
# The series' outline was set in the accent; a receded bar keeps no accent edge.
|
|
4609
|
+
$point.Format.Line.ForeColor.RGB = $pointColor
|
|
4610
|
+
}
|
|
4611
|
+
catch {}
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4499
4615
|
$chartResultValue = [ordered]@{ op = 'add_chart'; changed = $true; name = [string]$shape.Name; series = $seriesCount; categories = $pointCount }
|
|
4500
4616
|
if ($pageFit) { $chartResultValue.pageFit = $pageFit }
|
|
4501
4617
|
return $chartResultValue
|
|
@@ -13,7 +13,13 @@ import { applyPdfBatch } from '../pdf/pdf-adapter.mjs';
|
|
|
13
13
|
import { createChartPdf } from '../pdf/pdf-writer.mjs';
|
|
14
14
|
import { renderPdfPages } from '../pdf/pdf-render.mjs';
|
|
15
15
|
import { assertOfficeOperationContracts } from '../capabilities.mjs';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
expandFilledFormulas,
|
|
18
|
+
quoteUnquotedSheetReferences,
|
|
19
|
+
validateXlsxOperations,
|
|
20
|
+
} from '../portable/xlsx-contract.mjs';
|
|
21
|
+
import { translateSharedFormula } from '../portable/xlsx-formula-engine.mjs';
|
|
22
|
+
import { documentBrief } from '../quality/document-brief.mjs';
|
|
17
23
|
import { applyTabularBatch } from './tabular.mjs';
|
|
18
24
|
import { expandOfficeDesignOperations } from '../design/design-system.mjs';
|
|
19
25
|
import { createPptxSlideSelection } from '../design/library/design-library.mjs';
|
|
@@ -433,6 +439,12 @@ async function prepareBatchOperations(session, args) {
|
|
|
433
439
|
session.designRequest = designRequest;
|
|
434
440
|
session.design = prepared.design;
|
|
435
441
|
session.designState ||= emptyOfficeDesignState();
|
|
442
|
+
// A Word document or workbook written to a brief (design.brief, the deck's key: value lines) is held to it as an
|
|
443
|
+
// authored deck is: its figures against the facts, and a scored page review at finalize.
|
|
444
|
+
if (['docx', 'xlsx'].includes(session.format) && typeof args.design?.brief === 'string') {
|
|
445
|
+
const brief = documentBrief(args.design.brief);
|
|
446
|
+
if (brief) session.authoredBrief = brief;
|
|
447
|
+
}
|
|
436
448
|
const baseDir = args.__cwd || dirname(session.target);
|
|
437
449
|
let operations = Array.isArray(prepared.operations)
|
|
438
450
|
? prepared.operations.map((operation) => localizeOperation(operation, baseDir))
|
|
@@ -454,6 +466,9 @@ async function prepareBatchOperations(session, args) {
|
|
|
454
466
|
operations = await withImportedSlideBackgrounds(session, operations);
|
|
455
467
|
prepared.scratch = [];
|
|
456
468
|
operations = await withDocumentCharts(session, operations, prepared.scratch);
|
|
469
|
+
// set_formula over a range is written cell by cell, each formula moved as Excel's fill moves it, before either
|
|
470
|
+
// backend sees it: both then write cells they already write.
|
|
471
|
+
if (session.format === 'xlsx') operations = expandFilledFormulas(operations, translateSharedFormula);
|
|
457
472
|
if (session.format === 'xlsx' || TABULAR_FORMATS.has(session.format)) validateXlsxOperations(operations);
|
|
458
473
|
// Excel rejects `My Sheet!A1` outright; the portable writer quotes it from
|
|
459
474
|
// the sheet list, and an Excel session gets the same courtesy here.
|
|
@@ -55,7 +55,8 @@ async function structureIssues(session, args) {
|
|
|
55
55
|
if (!read?.document) return [];
|
|
56
56
|
return reviewOfficeStructure({
|
|
57
57
|
format: session.format,
|
|
58
|
-
|
|
58
|
+
// A PDF's designed sheets are held to the print type floors (assurance-structure-pptx typeFloors).
|
|
59
|
+
document: session.printSheet ? { ...read.document, printSheet: true } : read.document,
|
|
59
60
|
auditProfile: args.auditProfile,
|
|
60
61
|
});
|
|
61
62
|
} catch {
|
|
@@ -292,7 +293,7 @@ async function microsoftOfficeIssues(session, args) {
|
|
|
292
293
|
function portableIssues(session, args) {
|
|
293
294
|
if (session.format === 'pdf') return issuesPdf(session.target, args);
|
|
294
295
|
if (TABULAR_FORMATS.has(session.format)) return issuesTabular(session.target, session.format, args);
|
|
295
|
-
return issuesPortableOoxml(session.target, session.format, args);
|
|
296
|
+
return issuesPortableOoxml(session.target, session.format, session.printSheet ? { ...args, printSheet: true } : args);
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
export async function issues(session, args = {}) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// recalculate → review → hold checks → save → validate → close (see
|
|
3
3
|
// office-finalize/*.mjs for each stage).
|
|
4
4
|
import { recalculateForReview } from './office-recalculation.mjs';
|
|
5
|
-
import { reviewForFinalize } from './office-finalize/review-stage.mjs';
|
|
5
|
+
import { briefedDocument, reviewForFinalize } from './office-finalize/review-stage.mjs';
|
|
6
6
|
import { finalizeHold, reviewHold } from './office-finalize/holds.mjs';
|
|
7
7
|
import { closeForFinalize, saveForFinalize, validateForFinalize } from './office-finalize/commit-stage.mjs';
|
|
8
8
|
|
|
@@ -29,7 +29,11 @@ export async function finalize(session, args, cwd, signal) {
|
|
|
29
29
|
session,
|
|
30
30
|
failOn,
|
|
31
31
|
stepMetrics,
|
|
32
|
-
|
|
32
|
+
// A PDF of designed pages is the author's deck on paper and owes the deck's critique.
|
|
33
|
+
requiresVisualReview:
|
|
34
|
+
session.authoredFrame === true ||
|
|
35
|
+
briefedDocument(session) ||
|
|
36
|
+
(session.format === 'pptx' && session.designState?.requiresVisualReview === true),
|
|
33
37
|
recalculation: await timedStep('recalculation', () => recalculateForReview(session, signal)),
|
|
34
38
|
};
|
|
35
39
|
const { recalculation } = context;
|
|
@@ -40,6 +40,8 @@ export async function qa(session, args, cwd, { reuseRender = false } = {}) {
|
|
|
40
40
|
format: session.format,
|
|
41
41
|
pageRoles: design.pageRoles,
|
|
42
42
|
smallWorksheet: session.format === 'xlsx' && isSmallWorksheetDocument(design.currentSnapshot?.document),
|
|
43
|
+
pageCount: preview.pageCount,
|
|
44
|
+
designedPages: session.authoredFrame === true,
|
|
43
45
|
});
|
|
44
46
|
const trust = design.currentSnapshot?.trust || session.trustReview || null;
|
|
45
47
|
const { review, combinedIssuesAfter } = assembleQaReview({
|
|
@@ -92,5 +94,6 @@ export async function qa(session, args, cwd, { reuseRender = false } = {}) {
|
|
|
92
94
|
export async function render(session, args, cwd) {
|
|
93
95
|
await recalculateForReview(session, session.activeSignal || null);
|
|
94
96
|
const preview = await renderOfficePreview(session, args, cwd);
|
|
95
|
-
|
|
97
|
+
// A PDF of designed pages is reviewed as the deck it was measured as: receipt and contact sheet.
|
|
98
|
+
return session.format === 'pptx' || session.authoredFrame === true ? pptxReviewArtifacts(session, preview) : preview;
|
|
96
99
|
}
|