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
|
@@ -20,6 +20,11 @@ const AUTHORED_ADVISORY_SOURCES = new Set([
|
|
|
20
20
|
// with its chart laid over the figures it was drawn from.
|
|
21
21
|
const VISIBLE_DEFECT_CODES = new Set(['drawing_covers_cells', 'drawing_overlap']);
|
|
22
22
|
|
|
23
|
+
/** A Word document or workbook written to a brief (design.brief): reviewed with the deck's scored critique. */
|
|
24
|
+
export function briefedDocument(session) {
|
|
25
|
+
return ['docx', 'xlsx'].includes(session?.format) && session?.authoredBrief?.present === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export function blocksFinalize(issue, { failOn, authored }) {
|
|
24
29
|
if (
|
|
25
30
|
VISIBLE_DEFECT_CODES.has(String(issue?.code || '')) &&
|
|
@@ -79,11 +84,19 @@ export async function reviewForFinalize(session, args, cwd, { timedStep, failOn,
|
|
|
79
84
|
// only pixels this call produced are new evidence worth sending again.
|
|
80
85
|
const reviewImages = reviewed?.preview?.reused !== true && Array.isArray(reviewed?._images) ? reviewed._images : [];
|
|
81
86
|
const review = reviewed ? { ...reviewed } : null;
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// A PDF of designed pages (authoring/pdf-author-action.mjs) is reviewed as the deck it was measured as, and so is a
|
|
88
|
+
// Word document or workbook written to a brief: five scores, a note, and checks from the brief per page.
|
|
89
|
+
const scored = session.authoredFrame === true || briefedDocument(session);
|
|
90
|
+
const deck = session.format === 'pptx' || scored;
|
|
91
|
+
const pptx = deck ? applyPptxAcceptance(session, args, review) : null;
|
|
92
|
+
const documentVisualReview = scored ? null : applyDocumentAcceptance(session, args, review);
|
|
84
93
|
if (review) delete review._images;
|
|
85
94
|
const issuesAfter = review?.issuesAfter || [];
|
|
86
95
|
const blockingIssues = issuesAfter.filter((issue) => blocksFinalize(issue, { failOn, authored }));
|
|
96
|
+
// Its measured audit ran on the deck at author time; an error it found holds the file as it would the deck.
|
|
97
|
+
if (session.authoredFrame === true) {
|
|
98
|
+
blockingIssues.push(...(session.frameAudit?.top || []).filter((issue) => issue.severity === 'error'));
|
|
99
|
+
}
|
|
87
100
|
if (review && authored) {
|
|
88
101
|
review.advisoryIssues = issuesAfter.filter(
|
|
89
102
|
(issue) => !blockingIssues.includes(issue) && ['error', 'warning'].includes(String(issue?.severity || ''))
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { inferPptxSlideRoles, reviewOfficeDesign } from '../../quality/design-review.mjs';
|
|
5
5
|
import { issue } from '../../quality/assurance-issue.mjs';
|
|
6
6
|
import { reviewSnapshot } from '../office-actions-inspect.mjs';
|
|
7
|
+
import { documentBriefIssues } from '../../quality/document-brief.mjs';
|
|
7
8
|
|
|
8
9
|
function unavailableDesignReview(session, error) {
|
|
9
10
|
return {
|
|
@@ -64,6 +65,14 @@ export async function reviewQaDesign(session, args) {
|
|
|
64
65
|
} catch (error) {
|
|
65
66
|
designReview = unavailableDesignReview(session, error);
|
|
66
67
|
}
|
|
68
|
+
// A PDF of designed pages was read against its brief on the working deck at author time (the plan
|
|
69
|
+
// count, the facts' locators); those readings join the PDF's own review.
|
|
70
|
+
if (session.authoredFrame === true && Array.isArray(session.frameDesignIssues) && session.frameDesignIssues.length) {
|
|
71
|
+
designReview = { ...designReview, issues: [...(designReview.issues || []), ...session.frameDesignIssues] };
|
|
72
|
+
}
|
|
73
|
+
// A Word document or workbook written to a brief: its figures against the facts line, its facts against the sources.
|
|
74
|
+
const briefIssues = documentBriefIssues(session.format, currentSnapshot?.document, session.authoredBrief);
|
|
75
|
+
if (briefIssues.length) designReview = { ...designReview, issues: [...(designReview.issues || []), ...briefIssues] };
|
|
67
76
|
return {
|
|
68
77
|
designReview,
|
|
69
78
|
currentSnapshot,
|
|
@@ -9,6 +9,7 @@ import { describeOfficeCapabilities } from '../capabilities.mjs';
|
|
|
9
9
|
import { applyBatch, closeSession, finalize, issues, qa, render, save, validate } from './office-actions.mjs';
|
|
10
10
|
import { getOfficeElement, queryOfficeDocument } from './office-actions-read.mjs';
|
|
11
11
|
import { snapshot } from './office-sessions.mjs';
|
|
12
|
+
import { noteBatchAgainstHtml } from '../authoring/html-source-drift.mjs';
|
|
12
13
|
import {
|
|
13
14
|
assertTransactionUnchanged,
|
|
14
15
|
beginTransaction,
|
|
@@ -41,6 +42,8 @@ async function batchAction(session, args, cwd, signal) {
|
|
|
41
42
|
__cwd: cwd,
|
|
42
43
|
...(args.finalize === true ? { save: true } : {}),
|
|
43
44
|
});
|
|
45
|
+
const drift = noteBatchAgainstHtml(session, batch);
|
|
46
|
+
if (drift) batch.htmlSource = drift;
|
|
44
47
|
if (args.finalize !== true) return batch;
|
|
45
48
|
return {
|
|
46
49
|
...(await finalize(
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* returns null so the caller falls through to the session-bound dispatch.
|
|
10
10
|
*/
|
|
11
11
|
import { mkdir } from 'node:fs/promises';
|
|
12
|
-
import { dirname } from 'node:path';
|
|
12
|
+
import { dirname, extname } from 'node:path';
|
|
13
13
|
import { detectMicrosoftOffice } from '../com/com-adapter.mjs';
|
|
14
14
|
import { pdfOcrReadiness } from '../pdf/pdf-analysis.mjs';
|
|
15
15
|
import { describeOfficeCapabilities } from '../capabilities.mjs';
|
|
@@ -18,6 +18,8 @@ import { unicodeFontPath } from '../pdf/pdf-fonts.mjs';
|
|
|
18
18
|
import { inspectOfficeDesignLibrary } from '../design/library/design-library.mjs';
|
|
19
19
|
import { openCreateOrAttachOffice } from './office-actions-open.mjs';
|
|
20
20
|
import { authorPptx } from '../authoring/pptx-author-action.mjs';
|
|
21
|
+
import { authorPdf } from '../authoring/pdf-author-action.mjs';
|
|
22
|
+
import { authorHtmlDocument } from '../authoring/html-document-author.mjs';
|
|
21
23
|
import {
|
|
22
24
|
FILE_KIND_TO_FORMAT,
|
|
23
25
|
documentFormat,
|
|
@@ -97,7 +99,10 @@ async function describeWithoutSession(args, cwd) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
async function authorAction(args, cwd, dataDir, signal, startedAt) {
|
|
100
|
-
const
|
|
102
|
+
const extension = extname(String(args.path || args.output || '')).toLowerCase();
|
|
103
|
+
const author =
|
|
104
|
+
{ '.pdf': authorPdf, '.docx': authorHtmlDocument, '.xlsx': authorHtmlDocument }[extension] || authorPptx;
|
|
105
|
+
const authored = await author(args, { cwd, dataDir, signal });
|
|
101
106
|
const images = Array.isArray(authored?._images) ? authored._images : [];
|
|
102
107
|
delete authored._images;
|
|
103
108
|
const session = authored.session ? sessions.get(authored.session) : null;
|
|
@@ -145,7 +145,7 @@ function portableCreateDesignState() {
|
|
|
145
145
|
// old file is closed first, never left registered beside the new one (PDF) or
|
|
146
146
|
// handed back in place of the document the caller asked to write. A session on
|
|
147
147
|
// a document the user has open is theirs, so the overwrite is refused instead.
|
|
148
|
-
async function releaseOverwrittenSession(target, overwrite) {
|
|
148
|
+
export async function releaseOverwrittenSession(target, overwrite) {
|
|
149
149
|
if (overwrite !== true) return;
|
|
150
150
|
const existing = officeSessionForDocument(target);
|
|
151
151
|
if (!existing) return;
|
|
@@ -452,6 +452,22 @@ async function createPdfSession(creation, args, cwd) {
|
|
|
452
452
|
});
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// A PDF the author action printed from HTML (authoring/pdf-author-action.mjs)
|
|
456
|
+
// is already on disk; its session is registered as a created PDF's is, so it
|
|
457
|
+
// goes through the same render, review, and finalize.
|
|
458
|
+
export async function registerHtmlPdfSession(args, dataDir, target, { targetExisted, receipt }) {
|
|
459
|
+
const designContext = await resolveOfficeDesignContext({ args, dataDir, target, format: 'pdf', created: true });
|
|
460
|
+
return registerCreatedSession(
|
|
461
|
+
{ target, fileKind: documentFileKind(target), format: 'pdf', dataDir, targetExisted, designContext },
|
|
462
|
+
{
|
|
463
|
+
mode: 'portable',
|
|
464
|
+
backend: 'mixdog-pdf',
|
|
465
|
+
designState: portableCreateDesignState(),
|
|
466
|
+
extra: { createReceipt: receipt, ownership: 'owned', visible: false },
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
455
471
|
async function createTabularSession(creation) {
|
|
456
472
|
await mkdir(dirname(creation.target), { recursive: true });
|
|
457
473
|
await createTabular(creation.target);
|
|
@@ -571,7 +587,8 @@ function authoredDesignContext(resolved) {
|
|
|
571
587
|
export async function createAuthoredSession(args, _cwd, dataDir, target) {
|
|
572
588
|
const fileKind = documentFileKind(target);
|
|
573
589
|
const format = documentFormat(target);
|
|
574
|
-
if (
|
|
590
|
+
if (!['pptx', 'docx', 'xlsx'].includes(format))
|
|
591
|
+
throw new Error('author writes PowerPoint, Word, Excel, and PDF targets only');
|
|
575
592
|
const requestedMode = validatePptxAuthorMode(args.mode);
|
|
576
593
|
const selected = await selectMode(requestedMode, format, target);
|
|
577
594
|
const designContext = authoredDesignContext(
|
|
@@ -1151,7 +1151,7 @@ function drawPageFooters(document, properties, { font, margin, numbering }) {
|
|
|
1151
1151
|
});
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
-
function applyDocumentProperties(document, properties) {
|
|
1154
|
+
export function applyDocumentProperties(document, properties) {
|
|
1155
1155
|
if (properties.title != null) document.setTitle(String(properties.title));
|
|
1156
1156
|
if (properties.author != null) document.setAuthor(String(properties.author));
|
|
1157
1157
|
if (properties.subject != null) document.setSubject(String(properties.subject));
|
|
@@ -69,6 +69,27 @@ export function patchParagraphFormat(xml, properties, numbering = null) {
|
|
|
69
69
|
return patchWordFormat(xml, 'p', 'pPr', paragraphFormatXml(properties, numbering));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// A style definition's children run name … qFormat, then pPr, rPr, then the table parts; an absent pPr or rPr is
|
|
73
|
+
// opened in its place first, so the patch lands inside the schema's order rather than ahead of the style's name.
|
|
74
|
+
function openStyleProperties(styleXml, tag) {
|
|
75
|
+
if (new RegExp(`<w:${tag}(?:\\s[^>]*)?(?:/>|>)`).test(styleXml)) {
|
|
76
|
+
return styleXml.replace(new RegExp(`<w:${tag}(?:\\s[^>]*)?/>`), `<w:${tag}></w:${tag}>`);
|
|
77
|
+
}
|
|
78
|
+
const before = tag === 'pPr' ? /<w:(?:rPr|tblPr|trPr|tcPr|tblStylePr)\b/ : /<w:(?:tblPr|trPr|tcPr|tblStylePr)\b/;
|
|
79
|
+
const at = before.exec(styleXml)?.index ?? styleXml.lastIndexOf('</w:style>');
|
|
80
|
+
return `${styleXml.slice(0, at)}<w:${tag}></w:${tag}>${styleXml.slice(at)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A style's paragraph and run formatting patched with the supplied properties only (define_styles). */
|
|
84
|
+
export function patchWordStyle(styleXml, properties) {
|
|
85
|
+
let next = styleXml;
|
|
86
|
+
const paragraph = paragraphFormatXml(properties);
|
|
87
|
+
if (paragraph) next = patchWordFormat(openStyleProperties(next, 'pPr'), 'style', 'pPr', paragraph);
|
|
88
|
+
const run = wordRunProperties(properties);
|
|
89
|
+
if (run) next = patchWordFormat(openStyleProperties(next, 'rPr'), 'style', 'rPr', run);
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
/** Search the first body phrase, across text runs, without touching headers or footers.
|
|
73
94
|
* Only matching characters change; fields, drawings and breaks are search barriers. */
|
|
74
95
|
export function formatFirstBodyPhrase(documentXml, find, properties) {
|
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { access, chmod, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { gunzipSync } from 'node:zlib';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { withFileLock } from '../../shared/file-lock.mjs';
|
|
6
7
|
|
|
7
8
|
const VERSION = '0.3.0';
|
|
8
9
|
const MAX_ARCHIVE_BYTES = 160 * 1024 * 1024;
|
|
@@ -167,7 +168,10 @@ export async function ensureOoxmlValidator({ dataDir, download = true, signal =
|
|
|
167
168
|
reason: `OOXML schema validation is unavailable on ${process.platform}-${process.arch}.`,
|
|
168
169
|
};
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
+
// Test runners share immutable tools, never session files or user data.
|
|
172
|
+
// Normal app calls retain their existing data-directory cache.
|
|
173
|
+
const sharedCache = process.env.MIXDOG_TEST_OOXML_CACHE_DIR;
|
|
174
|
+
const paths = cachePaths(sharedCache || dataDir, entry);
|
|
171
175
|
if (await usableBinary(paths.binary)) {
|
|
172
176
|
return { available: true, cached: true, downloaded: false, path: paths.binary, version: VERSION };
|
|
173
177
|
}
|
|
@@ -181,6 +185,23 @@ export async function ensureOoxmlValidator({ dataDir, download = true, signal =
|
|
|
181
185
|
};
|
|
182
186
|
}
|
|
183
187
|
await mkdir(paths.root, { recursive: true });
|
|
188
|
+
if (sharedCache) {
|
|
189
|
+
return withFileLock(
|
|
190
|
+
join(paths.root, '.install.lock'),
|
|
191
|
+
async () => {
|
|
192
|
+
signal?.throwIfAborted();
|
|
193
|
+
if (await usableBinary(paths.binary)) {
|
|
194
|
+
return { available: true, cached: true, downloaded: false, path: paths.binary, version: VERSION };
|
|
195
|
+
}
|
|
196
|
+
return installValidator(paths, entry, signal);
|
|
197
|
+
},
|
|
198
|
+
{ timeoutMs: 120_000 }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return installValidator(paths, entry, signal);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function installValidator(paths, entry, signal) {
|
|
184
205
|
const temporary = join(paths.root, `.${entry.binary}.${randomUUID()}.tmp`);
|
|
185
206
|
try {
|
|
186
207
|
const archive = await fetchArchive(packageUrl(entry.name), { signal });
|
|
@@ -194,14 +194,18 @@ function categoryAxis({
|
|
|
194
194
|
reversed = horizontal,
|
|
195
195
|
size = CHART_TEXT.sheet.body,
|
|
196
196
|
low = false,
|
|
197
|
+
numberFormat = '',
|
|
197
198
|
} = {}) {
|
|
199
|
+
// Dated categories (a month column formatted m"월") read in their cells' format on a text axis: auto would turn
|
|
200
|
+
// them into a date scale, and without the linked format Excel labelled the months with their serials (46174).
|
|
198
201
|
return (
|
|
199
202
|
`<c:catAx><c:axId val="${CATEGORY_AXIS_ID}"/>` +
|
|
200
203
|
`<c:scaling><c:orientation val="${reversed ? 'maxMin' : 'minMax'}"/></c:scaling><c:delete val="${hidden ? 1 : 0}"/><c:axPos val="${horizontal ? 'l' : 'b'}"/>` +
|
|
204
|
+
(numberFormat ? `<c:numFmt formatCode="${xmlEncode(numberFormat)}" sourceLinked="1"/>` : '') +
|
|
201
205
|
`<c:majorTickMark val="none"/><c:minorTickMark val="none"/><c:tickLblPos val="${low ? 'low' : 'nextTo'}"/>` +
|
|
202
206
|
'<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="C7CBD1"/></a:solidFill></a:ln></c:spPr>' +
|
|
203
207
|
axisText(size) +
|
|
204
|
-
`<c:crossAx val="${VALUE_AXIS_ID}"/><c:crosses val="autoZero"/><c:auto val="1"/>` +
|
|
208
|
+
`<c:crossAx val="${VALUE_AXIS_ID}"/><c:crosses val="autoZero"/><c:auto val="${numberFormat ? 0 : 1}"/>` +
|
|
205
209
|
'<c:lblAlgn val="ctr"/><c:lblOffset val="100"/><c:noMultiLvlLbl val="0"/></c:catAx>'
|
|
206
210
|
);
|
|
207
211
|
}
|
|
@@ -288,6 +292,9 @@ export function chartXml({
|
|
|
288
292
|
font = '',
|
|
289
293
|
// CHART_TEXT.sheet or CHART_TEXT.slide.
|
|
290
294
|
text = CHART_TEXT.sheet,
|
|
295
|
+
// Dated categories: their serials and the cells' date format, written as a number reference Excel reads in that
|
|
296
|
+
// format; the labels in `categories` are the cache's text otherwise.
|
|
297
|
+
categoryDates = null,
|
|
291
298
|
} = {}) {
|
|
292
299
|
const family = resolveChartFamily(chartType);
|
|
293
300
|
if (!family) {
|
|
@@ -334,7 +341,7 @@ export function chartXml({
|
|
|
334
341
|
size: text.label,
|
|
335
342
|
pointColors: pointColorsFor(family, entry, rows),
|
|
336
343
|
}) +
|
|
337
|
-
`<c:cat>${stringReference(categoryFormula, rows)}</c:cat>` +
|
|
344
|
+
`<c:cat>${categoryDates ? numberReference(categoryFormula, categoryDates.values, categoryDates.format) : stringReference(categoryFormula, rows)}</c:cat>` +
|
|
338
345
|
`<c:val>${numberReference(valueFormula, entry.values, entry.numberFormat || valueNumberFormat)}</c:val>` +
|
|
339
346
|
(family.element === 'lineChart' ? '<c:smooth val="0"/>' : '') +
|
|
340
347
|
'</c:ser>'
|
|
@@ -386,6 +393,7 @@ export function chartXml({
|
|
|
386
393
|
reversed,
|
|
387
394
|
size: text.body,
|
|
388
395
|
low: negative,
|
|
396
|
+
numberFormat: categoryDates?.format || '',
|
|
389
397
|
})}${valueAxis({
|
|
390
398
|
size: text.body,
|
|
391
399
|
horizontal: family.direction === 'bar',
|
|
@@ -808,6 +808,12 @@ export async function setDocxHeaderFooter(zip, op) {
|
|
|
808
808
|
};
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
+
// A separator says the number stands against the total ("3 / 12"): asked for with separator:' / ' and no
|
|
812
|
+
// includeTotal, the footer printed "3" alone. includeTotal:false still keeps the number alone.
|
|
813
|
+
export function pageNumbersIncludeTotal(op) {
|
|
814
|
+
return op.includeTotal === true || (op.includeTotal !== false && Boolean(String(op.separator ?? '').trim()));
|
|
815
|
+
}
|
|
816
|
+
|
|
811
817
|
// Same vocabulary as set_header_footer: kind names the story (the footer
|
|
812
818
|
// unless the caller asks for the header), variant names the page.
|
|
813
819
|
export async function addDocxPageNumbers(zip, op) {
|
|
@@ -821,11 +827,11 @@ export async function addDocxPageNumbers(zip, op) {
|
|
|
821
827
|
// larger in the portable file than the same footer written through Word.
|
|
822
828
|
const size = '<w:rPr><w:sz w:val="18"/><w:szCs w:val="18"/></w:rPr>';
|
|
823
829
|
const prefix = op.prefix ? `<w:r>${size}<w:t xml:space="preserve">${xmlEncode(op.prefix)} </w:t></w:r>` : '';
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
830
|
+
const includeTotal = pageNumbersIncludeTotal(op);
|
|
831
|
+
const separator = includeTotal
|
|
832
|
+
? `<w:r>${size}<w:t xml:space="preserve"> ${xmlEncode(String(op.separator || '/').trim() || '/')} </w:t></w:r>` +
|
|
833
|
+
`<w:fldSimple w:instr=" NUMPAGES "><w:r>${size}<w:t>1</w:t></w:r></w:fldSimple>`
|
|
834
|
+
: '';
|
|
829
835
|
const numbering =
|
|
830
836
|
`<w:p><w:pPr><w:jc w:val="${alignment}"/><w:rPr><w:sz w:val="18"/><w:szCs w:val="18"/></w:rPr></w:pPr>${prefix}` +
|
|
831
837
|
`<w:fldSimple w:instr=" PAGE "><w:r>${size}<w:t>1</w:t></w:r></w:fldSimple>` +
|
|
@@ -857,7 +863,7 @@ export async function addDocxPageNumbers(zip, op) {
|
|
|
857
863
|
changed: true,
|
|
858
864
|
part: written.part,
|
|
859
865
|
header,
|
|
860
|
-
includeTotal
|
|
866
|
+
includeTotal,
|
|
861
867
|
...(written.replaced ? { replaced: true } : {}),
|
|
862
868
|
...(keptStory ? { keptExistingContent: true } : {}),
|
|
863
869
|
};
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
} from './portable-docx-xml.mjs';
|
|
35
35
|
import { docxRevisionTree, flattenDocxRevisions } from './docx-revisions.mjs';
|
|
36
36
|
import { settleDocxStory } from './docx-runs.mjs';
|
|
37
|
-
import { patchParagraphFormat } from './docx-formatting.mjs';
|
|
37
|
+
import { patchParagraphFormat, patchWordStyle } from './docx-formatting.mjs';
|
|
38
|
+
import { documentStyleId } from './portable-docx-styles.mjs';
|
|
38
39
|
import { anchorPhraseInParagraph, trackedParagraphReplace, trackedParagraphRewrite } from './docx-tracked-edits.mjs';
|
|
39
40
|
|
|
40
41
|
// Word rebuilds a TOC field when the reader updates it; until then the cached
|
|
@@ -386,6 +387,79 @@ export async function setDocxDocumentFont(zip, op) {
|
|
|
386
387
|
return { op: op.op, changed: true, ...font };
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
// The fields one style takes in define_styles: its face, size, weight, ink, and its paragraph's flow.
|
|
391
|
+
export const DOCX_STYLE_FIELDS = Object.freeze([
|
|
392
|
+
'name',
|
|
393
|
+
'nameEastAsia',
|
|
394
|
+
'size',
|
|
395
|
+
'bold',
|
|
396
|
+
'italic',
|
|
397
|
+
'color',
|
|
398
|
+
'alignment',
|
|
399
|
+
'spacingBefore',
|
|
400
|
+
'spacingAfter',
|
|
401
|
+
'lineSpacing',
|
|
402
|
+
'lineSpacingRule',
|
|
403
|
+
'keepWithNext',
|
|
404
|
+
'keepTogether',
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
/** The fields of every style asked for, checked before anything is written; one message lists every fault. */
|
|
408
|
+
export function docxStyleDefinitions(op) {
|
|
409
|
+
const styles = op.styles;
|
|
410
|
+
if (!styles || typeof styles !== 'object' || Array.isArray(styles) || !Object.keys(styles).length) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`define_styles needs styles: { "<style name>": { ${DOCX_STYLE_FIELDS.join(', ')} } }, e.g. { "Heading 1": { size: 15, bold: true } }`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const faults = [];
|
|
416
|
+
for (const [style, fields] of Object.entries(styles)) {
|
|
417
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
418
|
+
faults.push(`${style}: needs an object of fields`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const unknown = Object.keys(fields).filter((field) => !DOCX_STYLE_FIELDS.includes(field));
|
|
422
|
+
if (unknown.length) faults.push(`${style}: unknown fields ${unknown.join(', ')}`);
|
|
423
|
+
}
|
|
424
|
+
if (faults.length) throw new Error(`define_styles: ${faults.join('; ')}. Fields: ${DOCX_STYLE_FIELDS.join(', ')}.`);
|
|
425
|
+
return Object.entries(styles);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Word's own styles defined once — the title, headings, body, caption a report is set in — so every paragraph that
|
|
429
|
+
* names the style takes them, and a paragraph the reader adds in Word under that style matches the rest. Set
|
|
430
|
+
* paragraph by paragraph instead, a heading added later came out in Word's blue Calibri. */
|
|
431
|
+
export async function defineDocxStyles(zip, op) {
|
|
432
|
+
const definitions = docxStyleDefinitions(op);
|
|
433
|
+
const defined = [];
|
|
434
|
+
const notFound = [];
|
|
435
|
+
for (const [requested, fields] of definitions) {
|
|
436
|
+
const resolved = await documentStyleId(zip, requested);
|
|
437
|
+
if (!resolved.found) {
|
|
438
|
+
notFound.push(requested);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const styles = await zipText(zip, 'word/styles.xml');
|
|
442
|
+
const id = resolved.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
443
|
+
const pattern = new RegExp(`<w:style\\b(?=[^>]*\\bw:styleId="${id}")[^>]*>[\\s\\S]*?<\\/w:style>`);
|
|
444
|
+
const style = pattern.exec(styles)?.[0];
|
|
445
|
+
if (!style) {
|
|
446
|
+
notFound.push(requested);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
zip.file(
|
|
450
|
+
'word/styles.xml',
|
|
451
|
+
styles.replace(style, () => patchWordStyle(style, fields))
|
|
452
|
+
);
|
|
453
|
+
defined.push(requested);
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
op: op.op,
|
|
457
|
+
changed: defined.length > 0,
|
|
458
|
+
styles: defined,
|
|
459
|
+
...(notFound.length ? { styleNotFound: notFound } : {}),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
389
463
|
/** Page size, orientation, margins and text columns of one section, keeping what was not asked for. */
|
|
390
464
|
function pageRequest(properties) {
|
|
391
465
|
const orientation = String(properties.orientation || '').toLowerCase();
|
|
@@ -251,7 +251,15 @@ export function naturalTableColumnWidths(values, properties = {}, available = 0)
|
|
|
251
251
|
const headers = tableHeaderRows(properties, rows.length);
|
|
252
252
|
const gutters = isLayoutTable(properties) ? [] : gutterColumns(properties);
|
|
253
253
|
const measure = (text, rowIndex, column) =>
|
|
254
|
-
measureTextWidth(text, {
|
|
254
|
+
measureTextWidth(text, {
|
|
255
|
+
...font,
|
|
256
|
+
fontSize:
|
|
257
|
+
Number(tableRowStyle(properties, rowIndex)?.fontSize) > 0
|
|
258
|
+
? Number(tableRowStyle(properties, rowIndex).fontSize)
|
|
259
|
+
: font.fontSize,
|
|
260
|
+
bold: rowBold(properties, rowIndex, rows.length, headerBold && rowIndex < headers),
|
|
261
|
+
}) *
|
|
262
|
+
1.05 +
|
|
255
263
|
CELL_PADDING_POINTS +
|
|
256
264
|
(gutters[column] ? GUTTER_POINTS : 0);
|
|
257
265
|
return naturalColumnWidths(rows, measure, available);
|
|
@@ -284,6 +292,36 @@ function tableHeaderBold(properties, rows) {
|
|
|
284
292
|
return rows > 1 && properties?.headerBold !== false && properties?.repeatHeader !== false;
|
|
285
293
|
}
|
|
286
294
|
|
|
295
|
+
// One row's own type (rowStyles[row]: fontSize, color, bold): a stat strip sets its figures at 22 pt and the labels
|
|
296
|
+
// under them at 9 pt in one table, where a single fontSize set the labels at 22 pt too.
|
|
297
|
+
function tableRowStyle(properties, row) {
|
|
298
|
+
const style = Array.isArray(properties?.rowStyles) ? properties.rowStyles[row] : null;
|
|
299
|
+
return style && typeof style === 'object' ? style : null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// The last row is the total when totalRow is set: bold over a rule, so it reads as the sum of the rows above it and
|
|
303
|
+
// not as one more of them.
|
|
304
|
+
const isTotalRow = (properties, row, rows) => properties?.totalRow === true && rows > 1 && row === rows - 1;
|
|
305
|
+
export const TOTAL_ROW_RULE = Object.freeze({ size: 8, color: '374151' });
|
|
306
|
+
|
|
307
|
+
// A row's weight: its own rowStyles bold when given, else the header's or the total's.
|
|
308
|
+
function rowBold(properties, row, rows, header) {
|
|
309
|
+
const own = tableRowStyle(properties, row)?.bold;
|
|
310
|
+
if (typeof own === 'boolean') return own;
|
|
311
|
+
return header || isTotalRow(properties, row, rows);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// A row's type properties: the table's, with its own rowStyles size and ink over them.
|
|
315
|
+
function rowTypeProperties(properties, row) {
|
|
316
|
+
const style = tableRowStyle(properties, row);
|
|
317
|
+
if (!style) return properties;
|
|
318
|
+
return {
|
|
319
|
+
...properties,
|
|
320
|
+
...(Number(style.fontSize) > 0 ? { fontSize: Number(style.fontSize) } : {}),
|
|
321
|
+
...(style.color ? { color: style.color } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
287
325
|
const flushCellMargins = (first, last) => {
|
|
288
326
|
if (!first && !last) return '';
|
|
289
327
|
const left = first ? '<w:left w:w="0" w:type="dxa"/>' : '';
|
|
@@ -305,12 +343,15 @@ export function wordTableXml(operation, { available = 0 } = {}) {
|
|
|
305
343
|
[];
|
|
306
344
|
const heights = operation.properties?.rowHeights || [];
|
|
307
345
|
const justifications = (operation.properties?.columnAlignments || []).map(wordJustification);
|
|
308
|
-
const runProperties = wordTableRunProperties(operation.properties);
|
|
309
346
|
// The header row is set apart by weight, on both backends, unless the caller
|
|
310
347
|
// says otherwise; a header a reader cannot tell from the data is not one.
|
|
311
348
|
const headerBold = tableHeaderBold(operation.properties, rows);
|
|
312
|
-
const headerRunProperties = headerBold ? wordTableRunProperties(operation.properties, { bold: true }) : runProperties;
|
|
313
349
|
const headers = tableHeaderRows(operation.properties, rows);
|
|
350
|
+
const rowRunProperties = (row) =>
|
|
351
|
+
wordTableRunProperties(rowTypeProperties(operation.properties, row), {
|
|
352
|
+
bold: rowBold(operation.properties, row, rows, headerBold && row < headers),
|
|
353
|
+
});
|
|
354
|
+
const totalBorder = `<w:tcBorders><w:top w:val="single" w:sz="${TOTAL_ROW_RULE.size}" w:space="0" w:color="${TOTAL_ROW_RULE.color}"/></w:tcBorders>`;
|
|
314
355
|
const gutters = layout ? [] : gutterColumns(operation.properties);
|
|
315
356
|
// A page break never strands a table's edge: the header travels with the first two rows and the last two rows
|
|
316
357
|
// travel together (widow and orphan control, row by row) — a three-row table used to leave its last row alone at
|
|
@@ -319,7 +360,8 @@ export function wordTableXml(operation, { available = 0 } = {}) {
|
|
|
319
360
|
// caption): gluing every row instead carried a 25-row table whole to the next page and left half a page blank.
|
|
320
361
|
const keptRow = (row) =>
|
|
321
362
|
row < rows - 1 ? rows <= 6 || row <= headers || row === rows - 2 : Boolean(operation.properties?.keepWithNext);
|
|
322
|
-
const paragraphProperties = (row) =>
|
|
363
|
+
const paragraphProperties = (row) =>
|
|
364
|
+
wordTableParagraphProperties(rowTypeProperties(operation.properties, row), { keepNext: keptRow(row) });
|
|
323
365
|
const grid = Array.from(
|
|
324
366
|
{ length: columns },
|
|
325
367
|
(_, column) => `<w:gridCol${widths[column] ? ` w:w="${pointsToTwips(widths[column])}"` : ''}/>`
|
|
@@ -347,14 +389,15 @@ export function wordTableXml(operation, { available = 0 } = {}) {
|
|
|
347
389
|
.join('');
|
|
348
390
|
// Schema order inside pPr: style and spacing before justification.
|
|
349
391
|
const cellParagraphProperties = `${paragraphProperties(row)}${justifications[column] ? `<w:jc w:val="${justifications[column]}"/>` : ''}`;
|
|
350
|
-
const cellRunProperties = row
|
|
392
|
+
const cellRunProperties = rowRunProperties(row);
|
|
351
393
|
// A row reads from its top: a label beside a two-line note starts on the note's first line, where the
|
|
352
394
|
// bottom edge put it on the last. The header row sits on its rule (its bottom edge), so a header that wraps
|
|
353
395
|
// stands on the same line as the ones beside it. The exact line pitch keeps a Latin-only figure and a Hangul
|
|
354
396
|
// label on one baseline either way; set_table_cell_style verticalAlignment overrides per cell.
|
|
355
397
|
const margins = layout ? flushCellMargins(column === 0, column === columns - 1) : gutterMargin(gutters[column]);
|
|
356
398
|
const edge = row < headers && headerBold ? 'bottom' : 'top';
|
|
357
|
-
|
|
399
|
+
const rule = isTotalRow(operation.properties, row, rows) ? totalBorder : '';
|
|
400
|
+
return `<w:tc><w:tcPr>${width}${rule}${margins}<w:vAlign w:val="${edge}"/></w:tcPr><w:p>${cellParagraphProperties ? `<w:pPr>${cellParagraphProperties}</w:pPr>` : ''}<w:r>${cellRunProperties ? `<w:rPr>${cellRunProperties}</w:rPr>` : ''}${runs}</w:r></w:p></w:tc>`;
|
|
358
401
|
}
|
|
359
402
|
).join('')}</w:tr>`;
|
|
360
403
|
}).join('');
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from './portable-docx-edits.mjs';
|
|
29
29
|
import {
|
|
30
30
|
addDocxNote,
|
|
31
|
+
defineDocxStyles,
|
|
31
32
|
editDocxParagraph,
|
|
32
33
|
fillDocxContentControl,
|
|
33
34
|
fitDocxTable,
|
|
@@ -60,6 +61,7 @@ const DOCUMENT_EDITS = {
|
|
|
60
61
|
add_image: addDocxImage,
|
|
61
62
|
set_page: setDocxPage,
|
|
62
63
|
set_document_font: setDocxDocumentFont,
|
|
64
|
+
define_styles: defineDocxStyles,
|
|
63
65
|
insert_table_row: editDocxTableRowsOrColumns,
|
|
64
66
|
delete_table_row: editDocxTableRowsOrColumns,
|
|
65
67
|
insert_table_column: editDocxTableRowsOrColumns,
|
|
@@ -128,6 +128,8 @@ function tableRecord(block, tableIndex) {
|
|
|
128
128
|
path: `/body/tbl[${tableIndex}]/row[${rowIndex + 1}]/cell[${cellIndex + 1}]`,
|
|
129
129
|
index: cellIndex + 1,
|
|
130
130
|
text: blockText(cell.xml, 'w:t'),
|
|
131
|
+
// The cell's own type, as the review reads a paragraph's: a stat strip's label set at the figure's 22 pt.
|
|
132
|
+
...statedRunFont(cell.xml),
|
|
131
133
|
...cellMarkup(cell.xml),
|
|
132
134
|
})),
|
|
133
135
|
}));
|
|
@@ -621,7 +621,7 @@ async function sheetAuditIssues(zip) {
|
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
/** What the slides measure: fit, contrast, tracking, spacing, balance, structure, charts. */
|
|
624
|
-
async function presentationMetricIssues(zip) {
|
|
624
|
+
async function presentationMetricIssues(zip, options = {}) {
|
|
625
625
|
const issues = [];
|
|
626
626
|
const inspected = await inspectPptxTextBoxes(zip);
|
|
627
627
|
for (const fit of reviewTextBoxFit(inspected.boxes, {
|
|
@@ -654,7 +654,10 @@ async function presentationMetricIssues(zip) {
|
|
|
654
654
|
// neighbours share, a margin breach, a collision. A defect is cheapest to
|
|
655
655
|
// answer in the turn that wrote it, not one call later. Contrast stays with
|
|
656
656
|
// the metrics pass above, which measures it against the resolved surface.
|
|
657
|
-
|
|
657
|
+
// A PDF's designed sheets (printSheet) are held to the print type floors, not a screen's.
|
|
658
|
+
const document = await snapshotPptx(zip);
|
|
659
|
+
if (options.printSheet) document.printSheet = true;
|
|
660
|
+
for (const finding of reviewOfficeStructure({ format: 'pptx', document })) {
|
|
658
661
|
if (finding.code === 'low_contrast') continue;
|
|
659
662
|
issues.push(finding);
|
|
660
663
|
}
|
|
@@ -1060,7 +1063,7 @@ export async function issuesPortableOoxml(path, format, options = {}) {
|
|
|
1060
1063
|
issues.push(...(await placeholderIssues(zip, format)));
|
|
1061
1064
|
issues.push(...(await imagePlacementIssues(zip, format)));
|
|
1062
1065
|
if (format === 'xlsx') issues.push(...(await sheetAuditIssues(zip)));
|
|
1063
|
-
if (format === 'pptx') issues.push(...(await presentationMetricIssues(zip)));
|
|
1066
|
+
if (format === 'pptx') issues.push(...(await presentationMetricIssues(zip, options)));
|
|
1064
1067
|
issues.push(...packageProvenanceIssues(validation));
|
|
1065
1068
|
if (format === 'docx') issues.push(...(await documentContentIssues(zip, validation)));
|
|
1066
1069
|
else if (format === 'xlsx') issues.push(...(await workbookContentIssues(zip, options)));
|