mixdog 0.9.151 → 0.9.153
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/LICENSES/browser-import-NOTICE.txt +11 -0
- package/NOTICE.md +11 -0
- package/README.md +108 -68
- package/package.json +17 -4
- package/scripts/computer-task-cost-report.mjs +144 -0
- package/scripts/run-office-live-tests.mjs +32 -0
- package/scripts/tool-stress.mjs +0 -2
- package/src/agents/front-worker/AGENT.md +13 -0
- package/src/agents/front-worker/agent.json +6 -0
- package/src/agents/heavy-worker/AGENT.md +10 -11
- package/src/agents/heavy-worker/agent.json +1 -1
- package/src/agents/maintainer/AGENT.md +0 -2
- package/src/agents/reviewer/AGENT.md +9 -13
- package/src/agents/reviewer/agent.json +1 -1
- package/src/agents/security/AGENT.md +14 -0
- package/src/agents/security/agent.json +6 -0
- package/src/agents/worker/AGENT.md +8 -8
- package/src/agents/worker/agent.json +1 -1
- package/src/agents/writer/AGENT.md +14 -0
- package/src/agents/writer/agent.json +6 -0
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/rules/agent/00-common.md +4 -7
- package/src/rules/lead/01-general.md +0 -3
- package/src/rules/shared/10-tool-workflow.md +10 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
- package/src/runtime/agent/orchestrator/config.mjs +13 -68
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
- package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
- package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/pdf-extract.mjs +3 -0
- package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
- package/src/runtime/bridge-clients.test.mjs +827 -0
- package/src/runtime/browser-bridge/action-schema.mjs +251 -0
- package/src/runtime/browser-bridge/client.mjs +152 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
- package/src/runtime/computer-bridge/action-schema.mjs +653 -0
- package/src/runtime/computer-bridge/client.mjs +231 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/office/assurance-benchmark.mjs +358 -0
- package/src/runtime/office/assurance.mjs +833 -0
- package/src/runtime/office/benchmark.mjs +496 -0
- package/src/runtime/office/capabilities.mjs +551 -0
- package/src/runtime/office/com-adapter.mjs +283 -0
- package/src/runtime/office/content-model.mjs +183 -0
- package/src/runtime/office/contract-benchmark.mjs +105 -0
- package/src/runtime/office/design-library.mjs +1562 -0
- package/src/runtime/office/design-system.mjs +1966 -0
- package/src/runtime/office/index.mjs +2413 -0
- package/src/runtime/office/journal.mjs +82 -0
- package/src/runtime/office/office-assurance.test.mjs +371 -0
- package/src/runtime/office/office-com-host.ps1 +3781 -0
- package/src/runtime/office/office-com-session-host.ps1 +855 -0
- package/src/runtime/office/office-live-runtime.test.mjs +819 -0
- package/src/runtime/office/office-runtime.test.mjs +2070 -0
- package/src/runtime/office/ooxml-validator.mjs +271 -0
- package/src/runtime/office/pagination.mjs +132 -0
- package/src/runtime/office/pdf-adapter.mjs +591 -0
- package/src/runtime/office/pdf-analysis.mjs +372 -0
- package/src/runtime/office/pdf-render.mjs +167 -0
- package/src/runtime/office/pdf-security.mjs +63 -0
- package/src/runtime/office/polish-benchmark.mjs +190 -0
- package/src/runtime/office/portable-ooxml.mjs +1867 -0
- package/src/runtime/office/quality-live-benchmark.mjs +486 -0
- package/src/runtime/office/quality-pipeline.mjs +136 -0
- package/src/runtime/office/tabular.mjs +302 -0
- package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
- package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
- package/src/runtime/office/tool-defs.mjs +113 -0
- package/src/runtime/office/visual-diff.mjs +89 -0
- package/src/runtime/office/xlsx-assertions.mjs +172 -0
- package/src/runtime/office/xlsx-contract.mjs +147 -0
- package/src/runtime/shared/agent-route-config.mjs +9 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-execution-contract.mjs +16 -0
- package/src/runtime/shared/tool-surface.mjs +27 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/agent-disable.test.mjs +20 -1
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/deferred-tool-delta.mjs +104 -0
- package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
- package/src/session-runtime/goal-runtime.mjs +1141 -0
- package/src/session-runtime/goal-runtime.test.mjs +645 -0
- package/src/session-runtime/lifecycle-api.mjs +58 -11
- package/src/session-runtime/lifecycle-api.test.mjs +183 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/provider-request-snapshot.mjs +33 -9
- package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
- package/src/session-runtime/resource-api-global.test.mjs +109 -0
- package/src/session-runtime/resource-api.mjs +168 -116
- package/src/session-runtime/runtime-core.mjs +208 -29
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-lifecycle.mjs +34 -12
- package/src/session-runtime/session-title.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +37 -25
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-data.mjs +1 -1
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +42 -112
- package/src/session-runtime/tool-defs.mjs +6 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
- package/src/session-runtime/tool-surface.mjs +30 -8
- package/src/session-runtime/workflow-agents-api.mjs +8 -25
- package/src/session-runtime/workflow.mjs +8 -6
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/job-views.mjs +3 -1
- package/src/standalone/agent-tool/notify.mjs +11 -9
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/agent-tool/render.mjs +3 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
- package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool/worker-index.mjs +123 -0
- package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +57 -10
- package/src/standalone/daemon.mjs +113 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +709 -58
- package/src/standalone/session-runtime-inline-host.mjs +258 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +302 -26
- package/src/standalone/session-service-agent-tree.test.mjs +984 -0
- package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
- package/src/standalone/session-service.mjs +711 -11
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +187 -45
- package/src/tui/lib/voice-setup.mjs +4 -1
- package/src/tui/session/agent-message-source.test.mjs +37 -0
- package/src/tui/session/completion-card-restore.test.mjs +50 -0
- package/src/tui/session/context-state.mjs +3 -13
- package/src/tui/session/goal-continuation.mjs +141 -0
- package/src/tui/session/goal-continuation.test.mjs +203 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +190 -0
- package/src/tui/session/session-api-ext.mjs +48 -8
- package/src/tui/session/session-api.mjs +73 -1
- package/src/tui/session/session-flow.mjs +31 -1
- package/src/tui/session/turn.mjs +88 -15
- package/src/tui/session-local.mjs +39 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -0,0 +1,3781 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
3
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
4
|
+
|
|
5
|
+
function Emit-Json($value) {
|
|
6
|
+
[Console]::Out.WriteLine(($value | ConvertTo-Json -Depth 20 -Compress))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ProgId-ForFormat([string]$format) {
|
|
10
|
+
switch ($format.ToLowerInvariant()) {
|
|
11
|
+
'docx' { return 'Word.Application' }
|
|
12
|
+
'xlsx' { return 'Excel.Application' }
|
|
13
|
+
'pptx' { return 'PowerPoint.Application' }
|
|
14
|
+
default { return $null }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Office-SaveFormatForPath([string]$format, [string]$path) {
|
|
19
|
+
$extension = [System.IO.Path]::GetExtension($path).TrimStart('.').ToLowerInvariant()
|
|
20
|
+
switch ($format) {
|
|
21
|
+
'docx' {
|
|
22
|
+
switch ($extension) {
|
|
23
|
+
'docm' { return 13 }
|
|
24
|
+
'dotx' { return 14 }
|
|
25
|
+
'dotm' { return 15 }
|
|
26
|
+
default { return 16 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
'xlsx' {
|
|
30
|
+
switch ($extension) {
|
|
31
|
+
'xlsm' { return 52 }
|
|
32
|
+
'xltm' { return 53 }
|
|
33
|
+
'xltx' { return 54 }
|
|
34
|
+
default { return 51 }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
'pptx' {
|
|
38
|
+
switch ($extension) {
|
|
39
|
+
'pptm' { return 25 }
|
|
40
|
+
'potx' { return 26 }
|
|
41
|
+
'potm' { return 27 }
|
|
42
|
+
default { return 24 }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Collection-For($app, [string]$format) {
|
|
49
|
+
switch ($format) {
|
|
50
|
+
'docx' { return $app.Documents }
|
|
51
|
+
'xlsx' { return $app.Workbooks }
|
|
52
|
+
'pptx' { return $app.Presentations }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Find-OpenDocument($app, [string]$format, [string]$path) {
|
|
57
|
+
$full = [System.IO.Path]::GetFullPath($path)
|
|
58
|
+
foreach ($item in @(Collection-For $app $format)) {
|
|
59
|
+
try {
|
|
60
|
+
if ([string]::Equals([System.IO.Path]::GetFullPath([string]$item.FullName), $full, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
61
|
+
return $item
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
return $null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Active-Application([string]$progId) {
|
|
69
|
+
try {
|
|
70
|
+
return [System.Runtime.InteropServices.Marshal]::GetActiveObject($progId)
|
|
71
|
+
} catch {
|
|
72
|
+
return $null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function Installed([string]$progId) {
|
|
77
|
+
if (-not $progId) { return $false }
|
|
78
|
+
try { return $null -ne [type]::GetTypeFromProgID($progId) } catch { return $false }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function New-HiddenApplication([string]$format, [string]$progId) {
|
|
82
|
+
$app = New-Object -ComObject $progId
|
|
83
|
+
try { $app.DisplayAlerts = 0 } catch {}
|
|
84
|
+
try { $app.AutomationSecurity = 3 } catch {}
|
|
85
|
+
if ($format -ne 'pptx') {
|
|
86
|
+
try { $app.Visible = $false } catch {}
|
|
87
|
+
}
|
|
88
|
+
return $app
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Open-BackgroundDocument($app, [string]$format, [string]$path) {
|
|
92
|
+
switch ($format) {
|
|
93
|
+
'docx' { return $app.Documents.Open($path) }
|
|
94
|
+
'xlsx' {
|
|
95
|
+
$book = $app.Workbooks.Open($path)
|
|
96
|
+
try { $app.CalculateFullRebuild() } catch {}
|
|
97
|
+
return $book
|
|
98
|
+
}
|
|
99
|
+
'pptx' { return $app.Presentations.Open($path, $false, $false, $false) }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Snapshot-Word($doc, $payload) {
|
|
104
|
+
$paragraphs = @()
|
|
105
|
+
$paragraphOffset = if ($payload.paged) { [Math]::Max(0, [int]$payload.offset) } else { 0 }
|
|
106
|
+
$paragraphLimit = if ($payload.paged) { [Math]::Max(1, [int]$payload.limit) } else { [int]$doc.Paragraphs.Count }
|
|
107
|
+
if ($payload.target -and [string]$payload.target -match '^/body/p\[(\d+)\]') {
|
|
108
|
+
$paragraphOffset = [int]$Matches[1] - 1
|
|
109
|
+
$paragraphLimit = 1
|
|
110
|
+
}
|
|
111
|
+
$paragraphEnd = [Math]::Min([int]$doc.Paragraphs.Count, $paragraphOffset + $paragraphLimit)
|
|
112
|
+
for ($index = $paragraphOffset + 1; $index -le $paragraphEnd; $index++) {
|
|
113
|
+
$p = $doc.Paragraphs.Item($index)
|
|
114
|
+
$text = ([string]$p.Range.Text).TrimEnd("`r", "`a")
|
|
115
|
+
if ($text.Length -gt 0) {
|
|
116
|
+
$style = $p.Range.Style
|
|
117
|
+
$styleName = try { [string]$style.NameLocal } catch {
|
|
118
|
+
try { [string]$style.Name } catch { [string]$style }
|
|
119
|
+
}
|
|
120
|
+
$tabStops = @()
|
|
121
|
+
try {
|
|
122
|
+
for ($tabIndex = 1; $tabIndex -le $p.Format.TabStops.Count; $tabIndex++) {
|
|
123
|
+
$tab = $p.Format.TabStops.Item($tabIndex)
|
|
124
|
+
$tabStops += [ordered]@{
|
|
125
|
+
position = [double]$tab.Position
|
|
126
|
+
alignment = [int]$tab.Alignment
|
|
127
|
+
leader = [int]$tab.Leader
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
$paragraphs += [ordered]@{
|
|
132
|
+
path = "/body/p[$index]"
|
|
133
|
+
index = $index
|
|
134
|
+
text = $text
|
|
135
|
+
style = $styleName
|
|
136
|
+
start = [int]$p.Range.Start
|
|
137
|
+
end = [int]$p.Range.End
|
|
138
|
+
pageStart = $(try { [int]$p.Range.Information(3) } catch { 0 })
|
|
139
|
+
pageEnd = $(try {
|
|
140
|
+
$endRange = $p.Range.Duplicate
|
|
141
|
+
$endRange.Collapse(0)
|
|
142
|
+
[int]$endRange.Information(3)
|
|
143
|
+
} catch { 0 })
|
|
144
|
+
format = [ordered]@{
|
|
145
|
+
alignment = [int]$p.Format.Alignment
|
|
146
|
+
spacingBefore = [double]$p.Format.SpaceBefore
|
|
147
|
+
spacingAfter = [double]$p.Format.SpaceAfter
|
|
148
|
+
lineSpacing = [double]$p.Format.LineSpacing
|
|
149
|
+
keepWithNext = [int]$p.Format.KeepWithNext
|
|
150
|
+
pageBreakBefore = [int]$p.Format.PageBreakBefore
|
|
151
|
+
tabStops = $tabStops
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
$tables = @()
|
|
157
|
+
for ($i = 1; $i -le $doc.Tables.Count; $i++) {
|
|
158
|
+
$table = $doc.Tables.Item($i)
|
|
159
|
+
$rows = @()
|
|
160
|
+
for ($r = 1; $r -le $table.Rows.Count; $r++) {
|
|
161
|
+
$cells = @()
|
|
162
|
+
for ($c = 1; $c -le $table.Columns.Count; $c++) {
|
|
163
|
+
try { $cells += ([string]$table.Cell($r, $c).Range.Text).TrimEnd("`r", "`a") } catch { $cells += $null }
|
|
164
|
+
}
|
|
165
|
+
$rows += ,$cells
|
|
166
|
+
}
|
|
167
|
+
$tableRows = @()
|
|
168
|
+
for ($r = 1; $r -le $rows.Count; $r++) {
|
|
169
|
+
$cells = @()
|
|
170
|
+
for ($c = 1; $c -le @($rows[$r - 1]).Count; $c++) {
|
|
171
|
+
$cells += [ordered]@{ path = "/body/tbl[$i]/row[$r]/cell[$c]"; index = $c; text = @($rows[$r - 1])[$c - 1] }
|
|
172
|
+
}
|
|
173
|
+
$tableRows += [ordered]@{ path = "/body/tbl[$i]/row[$r]"; index = $r; cells = $cells }
|
|
174
|
+
}
|
|
175
|
+
$columnWidths = @()
|
|
176
|
+
try {
|
|
177
|
+
for ($columnIndex = 1; $columnIndex -le $table.Columns.Count; $columnIndex++) {
|
|
178
|
+
$columnWidths += [double]$table.Columns.Item($columnIndex).Width
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
try {
|
|
182
|
+
$firstRow = $table.Rows.Item(1)
|
|
183
|
+
for ($columnIndex = 1; $columnIndex -le $firstRow.Cells.Count; $columnIndex++) {
|
|
184
|
+
$columnWidths += [double]$firstRow.Cells.Item($columnIndex).Width
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
$tables += [ordered]@{
|
|
189
|
+
path = "/body/tbl[$i]"
|
|
190
|
+
index = $i
|
|
191
|
+
style = $(try { [string]$table.Style.NameLocal } catch { try { [string]$table.Style } catch { '' } })
|
|
192
|
+
alignment = $(try { [int]$table.Rows.Alignment } catch { 0 })
|
|
193
|
+
columnWidths = $columnWidths
|
|
194
|
+
rows = $tableRows
|
|
195
|
+
start = [int]$table.Range.Start
|
|
196
|
+
end = [int]$table.Range.End
|
|
197
|
+
pageStart = $(try {
|
|
198
|
+
$startRange = $table.Range.Duplicate
|
|
199
|
+
$startRange.Collapse(1)
|
|
200
|
+
[int]$startRange.Information(3)
|
|
201
|
+
} catch { 0 })
|
|
202
|
+
pageEnd = $(try {
|
|
203
|
+
$endRange = $table.Range.Duplicate
|
|
204
|
+
$endRange.Collapse(0)
|
|
205
|
+
[int]$endRange.Information(3)
|
|
206
|
+
} catch { 0 })
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
$blockOrder = @(
|
|
210
|
+
@($paragraphs | ForEach-Object {
|
|
211
|
+
[ordered]@{ type = 'paragraph'; index = [int]$_.index; path = [string]$_.path; start = [int]$_.start }
|
|
212
|
+
})
|
|
213
|
+
@($tables | ForEach-Object {
|
|
214
|
+
[ordered]@{ type = 'table'; index = [int]$_.index; path = [string]$_.path; start = [int]$_.start }
|
|
215
|
+
})
|
|
216
|
+
) | Sort-Object start
|
|
217
|
+
$sections = @()
|
|
218
|
+
for ($sectionIndex = 1; $sectionIndex -le $doc.Sections.Count; $sectionIndex++) {
|
|
219
|
+
$section = $doc.Sections.Item($sectionIndex)
|
|
220
|
+
$stories = @()
|
|
221
|
+
foreach ($kind in @(
|
|
222
|
+
[ordered]@{ name = 'primary'; value = 1 },
|
|
223
|
+
[ordered]@{ name = 'first'; value = 2 },
|
|
224
|
+
[ordered]@{ name = 'even'; value = 3 }
|
|
225
|
+
)) {
|
|
226
|
+
foreach ($location in @('header', 'footer')) {
|
|
227
|
+
try {
|
|
228
|
+
$collection = if ($location -eq 'header') { $section.Headers } else { $section.Footers }
|
|
229
|
+
$item = $collection.Item([int]$kind.value)
|
|
230
|
+
$stories += [ordered]@{
|
|
231
|
+
path = "/section[$sectionIndex]/${location}[$($kind.name)]"
|
|
232
|
+
kind = [string]$kind.name
|
|
233
|
+
location = $location
|
|
234
|
+
text = ([string]$item.Range.Text).TrimEnd("`r", "`a")
|
|
235
|
+
linkToPrevious = [bool]$item.LinkToPrevious
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
$sections += [ordered]@{
|
|
241
|
+
path = "/section[$sectionIndex]"
|
|
242
|
+
index = $sectionIndex
|
|
243
|
+
orientation = [int]$section.PageSetup.Orientation
|
|
244
|
+
topMargin = [double]$section.PageSetup.TopMargin
|
|
245
|
+
bottomMargin = [double]$section.PageSetup.BottomMargin
|
|
246
|
+
leftMargin = [double]$section.PageSetup.LeftMargin
|
|
247
|
+
rightMargin = [double]$section.PageSetup.RightMargin
|
|
248
|
+
stories = $stories
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
$images = @()
|
|
252
|
+
for ($imageIndex = 1; $imageIndex -le $doc.InlineShapes.Count; $imageIndex++) {
|
|
253
|
+
$image = $doc.InlineShapes.Item($imageIndex)
|
|
254
|
+
$images += [ordered]@{
|
|
255
|
+
path = "/body/image[$imageIndex]"
|
|
256
|
+
index = $imageIndex
|
|
257
|
+
type = [int]$image.Type
|
|
258
|
+
width = [double]$image.Width
|
|
259
|
+
height = [double]$image.Height
|
|
260
|
+
altText = $(try { [string]$image.AlternativeText } catch { '' })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
$comments = @()
|
|
264
|
+
for ($commentIndex = 1; $commentIndex -le $doc.Comments.Count; $commentIndex++) {
|
|
265
|
+
$comment = $doc.Comments.Item($commentIndex)
|
|
266
|
+
$isReply = $false
|
|
267
|
+
try { $isReply = $null -ne $comment.Ancestor } catch {}
|
|
268
|
+
if ($isReply) { continue }
|
|
269
|
+
$replies = @()
|
|
270
|
+
try {
|
|
271
|
+
for ($replyIndex = 1; $replyIndex -le $comment.Replies.Count; $replyIndex++) {
|
|
272
|
+
$reply = $comment.Replies.Item($replyIndex)
|
|
273
|
+
$replies += [ordered]@{
|
|
274
|
+
index = $replyIndex
|
|
275
|
+
author = $(try { [string]$reply.Author } catch { '' })
|
|
276
|
+
date = $(try { ([datetime]$reply.Date).ToUniversalTime().ToString('o') } catch { '' })
|
|
277
|
+
text = $(try { ([string]$reply.Range.Text).TrimEnd("`r", "`a") } catch { '' })
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch {}
|
|
281
|
+
$comments += [ordered]@{
|
|
282
|
+
path = "/body/comment[$commentIndex]"
|
|
283
|
+
index = $commentIndex
|
|
284
|
+
author = $(try { [string]$comment.Author } catch { '' })
|
|
285
|
+
initials = $(try { [string]$comment.Initial } catch { '' })
|
|
286
|
+
date = $(try { ([datetime]$comment.Date).ToUniversalTime().ToString('o') } catch { '' })
|
|
287
|
+
text = $(try { ([string]$comment.Range.Text).TrimEnd("`r", "`a") } catch { '' })
|
|
288
|
+
anchoredText = $(try { ([string]$comment.Scope.Text).TrimEnd("`r", "`a") } catch { '' })
|
|
289
|
+
resolved = $(try { [bool]$comment.Done } catch { $false })
|
|
290
|
+
replies = $replies
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
$revisionTypes = @{
|
|
294
|
+
1 = 'insertion'; 2 = 'deletion'; 3 = 'property'; 4 = 'paragraph_number'
|
|
295
|
+
5 = 'display_field'; 6 = 'reconcile'; 7 = 'conflict'; 8 = 'style'
|
|
296
|
+
9 = 'replacement'; 10 = 'paragraph_property'; 11 = 'table_property'
|
|
297
|
+
12 = 'section_property'; 13 = 'style_definition'; 14 = 'moved_from'
|
|
298
|
+
15 = 'moved_to'; 16 = 'cell_insertion'; 17 = 'cell_deletion'; 18 = 'cell_merge'
|
|
299
|
+
}
|
|
300
|
+
$revisions = @()
|
|
301
|
+
for ($revisionIndex = 1; $revisionIndex -le $doc.Revisions.Count; $revisionIndex++) {
|
|
302
|
+
$revision = $doc.Revisions.Item($revisionIndex)
|
|
303
|
+
$typeCode = [int]$revision.Type
|
|
304
|
+
$revisions += [ordered]@{
|
|
305
|
+
path = "/body/revision[$revisionIndex]"
|
|
306
|
+
index = $revisionIndex
|
|
307
|
+
type = $(if ($revisionTypes.ContainsKey($typeCode)) { [string]$revisionTypes[$typeCode] } else { 'unknown' })
|
|
308
|
+
typeCode = $typeCode
|
|
309
|
+
author = $(try { [string]$revision.Author } catch { '' })
|
|
310
|
+
date = $(try { ([datetime]$revision.Date).ToUniversalTime().ToString('o') } catch { '' })
|
|
311
|
+
text = $(try { ([string]$revision.Range.Text).TrimEnd("`r", "`a") } catch { '' })
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
$footnotes = @()
|
|
315
|
+
for ($noteIndex = 1; $noteIndex -le $doc.Footnotes.Count; $noteIndex++) {
|
|
316
|
+
$note = $doc.Footnotes.Item($noteIndex)
|
|
317
|
+
$footnotes += [ordered]@{
|
|
318
|
+
path = "/body/footnote[$noteIndex]"
|
|
319
|
+
index = $noteIndex
|
|
320
|
+
text = ([string]$note.Range.Text).TrimEnd("`r", "`a")
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
$endnotes = @()
|
|
324
|
+
for ($noteIndex = 1; $noteIndex -le $doc.Endnotes.Count; $noteIndex++) {
|
|
325
|
+
$note = $doc.Endnotes.Item($noteIndex)
|
|
326
|
+
$endnotes += [ordered]@{
|
|
327
|
+
path = "/body/endnote[$noteIndex]"
|
|
328
|
+
index = $noteIndex
|
|
329
|
+
text = ([string]$note.Range.Text).TrimEnd("`r", "`a")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
$contentControls = @()
|
|
333
|
+
for ($controlIndex = 1; $controlIndex -le $doc.ContentControls.Count; $controlIndex++) {
|
|
334
|
+
$control = $doc.ContentControls.Item($controlIndex)
|
|
335
|
+
$contentControls += [ordered]@{
|
|
336
|
+
path = "/body/content-control[$controlIndex]"
|
|
337
|
+
index = $controlIndex
|
|
338
|
+
tag = [string]$control.Tag
|
|
339
|
+
title = [string]$control.Title
|
|
340
|
+
lockContents = [bool]$control.LockContents
|
|
341
|
+
lockControl = [bool]$control.LockContentControl
|
|
342
|
+
text = ([string]$control.Range.Text).TrimEnd("`r", "`a")
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return [ordered]@{
|
|
346
|
+
format = 'docx'
|
|
347
|
+
path = [string]$doc.FullName
|
|
348
|
+
paragraphCount = $doc.Paragraphs.Count
|
|
349
|
+
tableCount = $doc.Tables.Count
|
|
350
|
+
commentCount = $comments.Count
|
|
351
|
+
revisionCount = $doc.Revisions.Count
|
|
352
|
+
comments = $comments
|
|
353
|
+
revisions = $revisions
|
|
354
|
+
footnoteCount = $footnotes.Count
|
|
355
|
+
endnoteCount = $endnotes.Count
|
|
356
|
+
footnotes = $footnotes
|
|
357
|
+
endnotes = $endnotes
|
|
358
|
+
contentControlCount = $contentControls.Count
|
|
359
|
+
contentControls = $contentControls
|
|
360
|
+
paragraphs = $paragraphs
|
|
361
|
+
tables = $tables
|
|
362
|
+
blockOrder = $blockOrder
|
|
363
|
+
sections = $sections
|
|
364
|
+
images = $images
|
|
365
|
+
pagination = $(if ($payload.paged) {
|
|
366
|
+
[ordered]@{
|
|
367
|
+
unit = 'paragraph'
|
|
368
|
+
offset = $paragraphOffset
|
|
369
|
+
limit = $paragraphLimit
|
|
370
|
+
returned = $paragraphs.Count
|
|
371
|
+
total = [int]$doc.Paragraphs.Count
|
|
372
|
+
nextOffset = $(if ($paragraphEnd -lt [int]$doc.Paragraphs.Count) { $paragraphEnd } else { $null })
|
|
373
|
+
}
|
|
374
|
+
} else { $null })
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function Cell-Value($cell) {
|
|
379
|
+
$value = $cell.Value2
|
|
380
|
+
if ($null -eq $value) { return $null }
|
|
381
|
+
if ($value -is [System.Array]) {
|
|
382
|
+
$rows = @()
|
|
383
|
+
for ($r = 1; $r -le $value.GetLength(0); $r++) {
|
|
384
|
+
$line = @()
|
|
385
|
+
for ($c = 1; $c -le $value.GetLength(1); $c++) { $line += $value[$r, $c] }
|
|
386
|
+
$rows += ,$line
|
|
387
|
+
}
|
|
388
|
+
return $rows
|
|
389
|
+
}
|
|
390
|
+
return $value
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function Excel-FormulaPrecedents([string]$formula, [string]$currentSheet) {
|
|
394
|
+
$output = @()
|
|
395
|
+
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
396
|
+
$clean = $formula.Replace("'", '')
|
|
397
|
+
foreach ($match in [regex]::Matches($clean, '(?:(?<sheet>[A-Za-z0-9_ .-]+)!)?\$?(?<col>[A-Z]{1,3})\$?(?<row>[1-9]\d*)')) {
|
|
398
|
+
$sheet = if ($match.Groups['sheet'].Success) { [string]$match.Groups['sheet'].Value } else { $currentSheet }
|
|
399
|
+
$ref = "$([string]$match.Groups['col'].Value)$([string]$match.Groups['row'].Value)"
|
|
400
|
+
$key = "$sheet!$ref"
|
|
401
|
+
if ($seen.Add($key)) {
|
|
402
|
+
$output += [ordered]@{ sheet = $sheet; ref = $ref; path = "/sheet[$sheet]/cell[$ref]" }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return $output
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function Snapshot-Excel($book, $payload) {
|
|
409
|
+
$sheets = @()
|
|
410
|
+
foreach ($sheet in @($book.Worksheets)) {
|
|
411
|
+
$used = $sheet.UsedRange
|
|
412
|
+
$entry = [ordered]@{
|
|
413
|
+
path = "/sheet[$([string]$sheet.Name)]"
|
|
414
|
+
name = [string]$sheet.Name
|
|
415
|
+
rows = [int]$used.Rows.Count
|
|
416
|
+
columns = [int]$used.Columns.Count
|
|
417
|
+
visible = [int]$sheet.Visible
|
|
418
|
+
pageSetup = [ordered]@{
|
|
419
|
+
printArea = $(try { [string]$sheet.PageSetup.PrintArea } catch { '' })
|
|
420
|
+
zoom = $(try { $sheet.PageSetup.Zoom } catch { $null })
|
|
421
|
+
fitToPagesWide = $(try { $sheet.PageSetup.FitToPagesWide } catch { $null })
|
|
422
|
+
fitToPagesTall = $(try { $sheet.PageSetup.FitToPagesTall } catch { $null })
|
|
423
|
+
orientation = $(try { [int]$sheet.PageSetup.Orientation } catch { 0 })
|
|
424
|
+
paperSize = $(try { [int]$sheet.PageSetup.PaperSize } catch { 0 })
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
$tables = @()
|
|
428
|
+
for ($tableIndex = 1; $tableIndex -le $sheet.ListObjects.Count; $tableIndex++) {
|
|
429
|
+
$table = $sheet.ListObjects.Item($tableIndex)
|
|
430
|
+
$tables += [ordered]@{
|
|
431
|
+
path = "/sheet[$([string]$sheet.Name)]/table[$tableIndex]"
|
|
432
|
+
index = $tableIndex
|
|
433
|
+
name = [string]$table.Name
|
|
434
|
+
range = [string]$table.Range.Address($false, $false)
|
|
435
|
+
style = [string]$table.TableStyle
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
$charts = @()
|
|
439
|
+
for ($chartIndex = 1; $chartIndex -le $sheet.ChartObjects().Count; $chartIndex++) {
|
|
440
|
+
$chartObject = $sheet.ChartObjects().Item($chartIndex)
|
|
441
|
+
$chart = $chartObject.Chart
|
|
442
|
+
$series = @()
|
|
443
|
+
try {
|
|
444
|
+
for ($seriesIndex = 1; $seriesIndex -le $chart.SeriesCollection().Count; $seriesIndex++) {
|
|
445
|
+
$seriesItem = $chart.SeriesCollection().Item($seriesIndex)
|
|
446
|
+
$series += [ordered]@{ index = $seriesIndex; name = [string]$seriesItem.Name; formula = [string]$seriesItem.Formula }
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
$charts += [ordered]@{
|
|
450
|
+
path = "/sheet[$([string]$sheet.Name)]/chart[$chartIndex]"
|
|
451
|
+
index = $chartIndex
|
|
452
|
+
name = [string]$chartObject.Name
|
|
453
|
+
chartType = [int]$chart.ChartType
|
|
454
|
+
title = $(if ($chart.HasTitle) { [string]$chart.ChartTitle.Text } else { '' })
|
|
455
|
+
left = [double]$chartObject.Left
|
|
456
|
+
top = [double]$chartObject.Top
|
|
457
|
+
width = [double]$chartObject.Width
|
|
458
|
+
height = [double]$chartObject.Height
|
|
459
|
+
series = $series
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
$pivots = @()
|
|
463
|
+
for ($pivotIndex = 1; $pivotIndex -le $sheet.PivotTables().Count; $pivotIndex++) {
|
|
464
|
+
$pivot = $sheet.PivotTables().Item($pivotIndex)
|
|
465
|
+
$pivots += [ordered]@{
|
|
466
|
+
path = "/sheet[$([string]$sheet.Name)]/pivot[$pivotIndex]"
|
|
467
|
+
index = $pivotIndex
|
|
468
|
+
name = [string]$pivot.Name
|
|
469
|
+
range = [string]$pivot.TableRange2.Address($false, $false)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
$validations = @()
|
|
473
|
+
try {
|
|
474
|
+
$validationCells = $sheet.Cells.SpecialCells(-4174)
|
|
475
|
+
for ($validationIndex = 1; $validationIndex -le $validationCells.Areas.Count; $validationIndex++) {
|
|
476
|
+
$area = $validationCells.Areas.Item($validationIndex)
|
|
477
|
+
$validation = $area.Cells.Item(1, 1).Validation
|
|
478
|
+
$validations += [ordered]@{
|
|
479
|
+
path = "/sheet[$([string]$sheet.Name)]/validation[$validationIndex]"
|
|
480
|
+
index = $validationIndex
|
|
481
|
+
ranges = @([string]$area.Address($false, $false))
|
|
482
|
+
type = [int]$validation.Type
|
|
483
|
+
alertStyle = [int]$validation.AlertStyle
|
|
484
|
+
operator = [int]$validation.Operator
|
|
485
|
+
allowBlank = [bool]$validation.IgnoreBlank
|
|
486
|
+
inCellDropdown = [bool]$validation.InCellDropdown
|
|
487
|
+
showInputMessage = [bool]$validation.ShowInput
|
|
488
|
+
showErrorMessage = [bool]$validation.ShowError
|
|
489
|
+
inputTitle = [string]$validation.InputTitle
|
|
490
|
+
inputMessage = [string]$validation.InputMessage
|
|
491
|
+
errorTitle = [string]$validation.ErrorTitle
|
|
492
|
+
errorMessage = [string]$validation.ErrorMessage
|
|
493
|
+
formula1 = $(try { [string]$validation.Formula1 } catch { '' })
|
|
494
|
+
formula2 = $(try { [string]$validation.Formula2 } catch { '' })
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} catch {}
|
|
498
|
+
$conditionalFormats = @()
|
|
499
|
+
try {
|
|
500
|
+
$conditionRange = $used
|
|
501
|
+
if ($payload.range -and [string]::Equals([string]$payload.sheet, [string]$sheet.Name, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
502
|
+
$conditionRange = $sheet.Range([string]$payload.range)
|
|
503
|
+
}
|
|
504
|
+
for ($conditionIndex = 1; $conditionIndex -le $conditionRange.FormatConditions.Count; $conditionIndex++) {
|
|
505
|
+
$condition = $conditionRange.FormatConditions.Item($conditionIndex)
|
|
506
|
+
$conditionalFormats += [ordered]@{
|
|
507
|
+
path = "/sheet[$([string]$sheet.Name)]/conditional-format[$conditionIndex]"
|
|
508
|
+
index = $conditionIndex
|
|
509
|
+
range = $(try { [string]$condition.AppliesTo.Address($false, $false) } catch { [string]$conditionRange.Address($false, $false) })
|
|
510
|
+
type = $(try { [int]$condition.Type } catch { 0 })
|
|
511
|
+
operator = $(try { [int]$condition.Operator } catch { 0 })
|
|
512
|
+
formula1 = $(try { [string]$condition.Formula1 } catch { '' })
|
|
513
|
+
formula2 = $(try { [string]$condition.Formula2 } catch { '' })
|
|
514
|
+
priority = $(try { [int]$condition.Priority } catch { 0 })
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch {}
|
|
518
|
+
$notes = @()
|
|
519
|
+
try {
|
|
520
|
+
$commentCells = $sheet.Cells.SpecialCells(-4144)
|
|
521
|
+
foreach ($cell in @($commentCells.Cells)) {
|
|
522
|
+
$notes += [ordered]@{
|
|
523
|
+
path = "/sheet[$([string]$sheet.Name)]/cell[$([string]$cell.Address($false, $false))]/note"
|
|
524
|
+
cell = [string]$cell.Address($false, $false)
|
|
525
|
+
text = [string]$cell.Comment.Text()
|
|
526
|
+
author = [string]$cell.Comment.Author
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
$mergedRanges = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
531
|
+
$mergeScanCount = 0
|
|
532
|
+
for ($mergeRow = 1; $mergeRow -le $used.Rows.Count -and $mergeScanCount -lt 2000; $mergeRow++) {
|
|
533
|
+
for ($mergeColumn = 1; $mergeColumn -le $used.Columns.Count -and $mergeScanCount -lt 2000; $mergeColumn++) {
|
|
534
|
+
$mergeScanCount++
|
|
535
|
+
try {
|
|
536
|
+
$cell = $used.Cells.Item($mergeRow, $mergeColumn)
|
|
537
|
+
if ($cell.MergeCells) { $null = $mergedRanges.Add([string]$cell.MergeArea.Address($false, $false)) }
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
$freezePanes = $null
|
|
542
|
+
if ([string]::Equals([string]$book.ActiveSheet.Name, [string]$sheet.Name, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
543
|
+
try {
|
|
544
|
+
$window = $book.Windows.Item(1)
|
|
545
|
+
$freezePanes = [ordered]@{
|
|
546
|
+
frozen = [bool]$window.FreezePanes
|
|
547
|
+
splitRow = [int]$window.SplitRow
|
|
548
|
+
splitColumn = [int]$window.SplitColumn
|
|
549
|
+
}
|
|
550
|
+
} catch {}
|
|
551
|
+
}
|
|
552
|
+
$autoFilter = $null
|
|
553
|
+
try {
|
|
554
|
+
if ($sheet.AutoFilterMode -and $null -ne $sheet.AutoFilter) {
|
|
555
|
+
$autoFilter = [ordered]@{ enabled = $true; range = [string]$sheet.AutoFilter.Range.Address($false, $false) }
|
|
556
|
+
}
|
|
557
|
+
} catch {}
|
|
558
|
+
$protection = [ordered]@{
|
|
559
|
+
contents = [bool]$sheet.ProtectContents
|
|
560
|
+
drawingObjects = [bool]$sheet.ProtectDrawingObjects
|
|
561
|
+
scenarios = [bool]$sheet.ProtectScenarios
|
|
562
|
+
}
|
|
563
|
+
$entry.tables = $tables
|
|
564
|
+
$entry.charts = $charts
|
|
565
|
+
$entry.pivots = $pivots
|
|
566
|
+
$entry.validationCount = $validations.Count
|
|
567
|
+
$entry.validations = $validations
|
|
568
|
+
$entry.conditionalFormatCount = $conditionalFormats.Count
|
|
569
|
+
$entry.conditionalFormats = $conditionalFormats
|
|
570
|
+
$entry.noteCount = $notes.Count
|
|
571
|
+
$entry.notes = $notes
|
|
572
|
+
$entry.mergedRanges = @($mergedRanges) | Sort-Object
|
|
573
|
+
$entry.freezePanes = $freezePanes
|
|
574
|
+
$entry.autoFilter = $autoFilter
|
|
575
|
+
$entry.protection = $protection
|
|
576
|
+
$cellCount = [int]$used.Rows.Count * [int]$used.Columns.Count
|
|
577
|
+
if ($cellCount -le 500) {
|
|
578
|
+
$cells = @()
|
|
579
|
+
for ($r = 1; $r -le $used.Rows.Count; $r++) {
|
|
580
|
+
for ($c = 1; $c -le $used.Columns.Count; $c++) {
|
|
581
|
+
$cell = $used.Cells.Item($r, $c)
|
|
582
|
+
if ($null -ne $cell.Value2 -or $cell.HasFormula) {
|
|
583
|
+
$address = ([string]$cell.Address($false, $false)).Replace('$', '')
|
|
584
|
+
$cells += [ordered]@{
|
|
585
|
+
path = "/sheet[$([string]$sheet.Name)]/cell[$address]"
|
|
586
|
+
ref = $address
|
|
587
|
+
value = $cell.Value2
|
|
588
|
+
formula = $(if ($cell.HasFormula) { [string]$cell.Formula } else { $null })
|
|
589
|
+
style = [ordered]@{
|
|
590
|
+
fontName = [string]$cell.Font.Name
|
|
591
|
+
fontSize = [double]$cell.Font.Size
|
|
592
|
+
bold = [bool]$cell.Font.Bold
|
|
593
|
+
italic = [bool]$cell.Font.Italic
|
|
594
|
+
color = [double]$cell.Font.Color
|
|
595
|
+
fillColor = [double]$cell.Interior.Color
|
|
596
|
+
numberFormat = [string]$cell.NumberFormat
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
$entry.cells = $cells
|
|
603
|
+
$entry.formulaLineage = @($cells | Where-Object { $_.formula } | ForEach-Object {
|
|
604
|
+
[ordered]@{
|
|
605
|
+
path = "$($_.path)/lineage"
|
|
606
|
+
from = $_.path
|
|
607
|
+
formula = $_.formula
|
|
608
|
+
precedents = @(Excel-FormulaPrecedents ([string]$_.formula) ([string]$sheet.Name))
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
$entry.lineageCount = $entry.formulaLineage.Count
|
|
612
|
+
}
|
|
613
|
+
if ($payload.sheet -and [string]::Equals([string]$payload.sheet, [string]$sheet.Name, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
614
|
+
$range = $used
|
|
615
|
+
if ($payload.range) { $range = $sheet.Range([string]$payload.range) }
|
|
616
|
+
if (($range.Rows.Count * $range.Columns.Count) -le 2000) {
|
|
617
|
+
$entry.values = Cell-Value $range
|
|
618
|
+
$entry.address = [string]$range.Address()
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
$sheets += $entry
|
|
622
|
+
}
|
|
623
|
+
$definedNames = @()
|
|
624
|
+
for ($nameIndex = 1; $nameIndex -le $book.Names.Count; $nameIndex++) {
|
|
625
|
+
try {
|
|
626
|
+
$name = $book.Names.Item($nameIndex)
|
|
627
|
+
$definedNames += [ordered]@{
|
|
628
|
+
path = "/defined-name[$nameIndex]"
|
|
629
|
+
index = $nameIndex
|
|
630
|
+
name = [string]$name.Name
|
|
631
|
+
nameLocal = [string]$name.NameLocal
|
|
632
|
+
refersTo = [string]$name.RefersTo
|
|
633
|
+
refersToLocal = [string]$name.RefersToLocal
|
|
634
|
+
visible = [bool]$name.Visible
|
|
635
|
+
}
|
|
636
|
+
} catch {}
|
|
637
|
+
}
|
|
638
|
+
return [ordered]@{
|
|
639
|
+
format = 'xlsx'
|
|
640
|
+
path = [string]$book.FullName
|
|
641
|
+
activeSheet = [string]$book.ActiveSheet.Name
|
|
642
|
+
definedNameCount = $definedNames.Count
|
|
643
|
+
definedNames = $definedNames
|
|
644
|
+
sheets = $sheets
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function Excel-ColumnLabel([int]$column) {
|
|
649
|
+
$label = ''
|
|
650
|
+
while ($column -gt 0) {
|
|
651
|
+
$column--
|
|
652
|
+
$label = [char](65 + ($column % 26)) + $label
|
|
653
|
+
$column = [Math]::Floor($column / 26)
|
|
654
|
+
}
|
|
655
|
+
return $label
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function Matrix-Item($matrix, [int]$row, [int]$column) {
|
|
659
|
+
if ($matrix -is [System.Array]) {
|
|
660
|
+
if ($matrix.Rank -eq 2) {
|
|
661
|
+
return $matrix.GetValue(
|
|
662
|
+
$matrix.GetLowerBound(0) + $row - 1,
|
|
663
|
+
$matrix.GetLowerBound(1) + $column - 1
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
return $matrix.GetValue($matrix.GetLowerBound(0) + $row - 1)
|
|
667
|
+
}
|
|
668
|
+
if ($row -eq 1 -and $column -eq 1) { return $matrix }
|
|
669
|
+
return $null
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function Snapshot-ExcelPage($book, $payload) {
|
|
673
|
+
$sheet = $book.ActiveSheet
|
|
674
|
+
if ($payload.sheet) { $sheet = $book.Worksheets.Item([string]$payload.sheet) }
|
|
675
|
+
$base = $sheet.UsedRange
|
|
676
|
+
if ($payload.range) { $base = $sheet.Range([string]$payload.range) }
|
|
677
|
+
$rowCount = [int]$base.Rows.Count
|
|
678
|
+
$columnCount = [int]$base.Columns.Count
|
|
679
|
+
$total = [int64]$rowCount * [int64]$columnCount
|
|
680
|
+
$offset = [Math]::Max(0, [int64]$payload.offset)
|
|
681
|
+
$limit = [Math]::Min(10000, [Math]::Max(1, [int]$payload.limit))
|
|
682
|
+
if ($offset -gt $total) { $offset = $total }
|
|
683
|
+
$remaining = [Math]::Min([int64]$limit, $total - $offset)
|
|
684
|
+
$cursor = $offset
|
|
685
|
+
$detailed = [bool]$payload.includeStyles -or $total -le 500
|
|
686
|
+
$cells = [System.Collections.Generic.List[object]]::new()
|
|
687
|
+
$rowBlocks = [System.Collections.Generic.List[object]]::new()
|
|
688
|
+
$formulaEntries = [System.Collections.Generic.List[object]]::new()
|
|
689
|
+
$populated = 0
|
|
690
|
+
while ($remaining -gt 0) {
|
|
691
|
+
$rowOffset = [Math]::Floor($cursor / $columnCount)
|
|
692
|
+
$columnOffset = [int]($cursor % $columnCount)
|
|
693
|
+
if ($columnOffset -eq 0 -and $remaining -ge $columnCount) {
|
|
694
|
+
$takeRows = [Math]::Floor($remaining / $columnCount)
|
|
695
|
+
$take = [int64]$takeRows * $columnCount
|
|
696
|
+
$range = $sheet.Range(
|
|
697
|
+
$base.Cells.Item($rowOffset + 1, 1),
|
|
698
|
+
$base.Cells.Item($rowOffset + $takeRows, $columnCount)
|
|
699
|
+
)
|
|
700
|
+
} else {
|
|
701
|
+
$take = [Math]::Min($remaining, $columnCount - $columnOffset)
|
|
702
|
+
$takeRows = 1
|
|
703
|
+
$range = $sheet.Range(
|
|
704
|
+
$base.Cells.Item($rowOffset + 1, $columnOffset + 1),
|
|
705
|
+
$base.Cells.Item($rowOffset + 1, $columnOffset + $take)
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
$values = $range.Value2
|
|
709
|
+
$formulas = $range.Formula
|
|
710
|
+
$rangeRows = [int]$range.Rows.Count
|
|
711
|
+
$rangeColumns = [int]$range.Columns.Count
|
|
712
|
+
$rangeStartRow = [int]$range.Row
|
|
713
|
+
$rangeStartColumn = [int]$range.Column
|
|
714
|
+
$blockValues = $null
|
|
715
|
+
$blockFormulas = $null
|
|
716
|
+
if (-not $detailed) {
|
|
717
|
+
$blockValues = [object[]]::new($rangeRows)
|
|
718
|
+
$blockFormulas = [System.Collections.Generic.List[object]]::new()
|
|
719
|
+
}
|
|
720
|
+
for ($r = 1; $r -le $rangeRows; $r++) {
|
|
721
|
+
$line = $null
|
|
722
|
+
if (-not $detailed) { $line = [object[]]::new($rangeColumns) }
|
|
723
|
+
for ($c = 1; $c -le $rangeColumns; $c++) {
|
|
724
|
+
$value = Matrix-Item $values $r $c
|
|
725
|
+
$formulaValue = Matrix-Item $formulas $r $c
|
|
726
|
+
$formula = if ([string]$formulaValue -like '=*') { [string]$formulaValue } else { $null }
|
|
727
|
+
if (-not $detailed) { $line[$c - 1] = $value }
|
|
728
|
+
if ($null -eq $value -and $null -eq $formula) { continue }
|
|
729
|
+
$populated++
|
|
730
|
+
$absoluteRow = $null
|
|
731
|
+
$absoluteColumn = $null
|
|
732
|
+
$address = $null
|
|
733
|
+
if ($detailed -or $formula) {
|
|
734
|
+
$absoluteRow = $rangeStartRow + $r - 1
|
|
735
|
+
$absoluteColumn = $rangeStartColumn + $c - 1
|
|
736
|
+
$address = "$(Excel-ColumnLabel $absoluteColumn)$absoluteRow"
|
|
737
|
+
}
|
|
738
|
+
if ($formula) {
|
|
739
|
+
$formulaEntry = [ordered]@{
|
|
740
|
+
path = "/sheet[$([string]$sheet.Name)]/cell[$address]"
|
|
741
|
+
ref = $address
|
|
742
|
+
row = $absoluteRow
|
|
743
|
+
column = $absoluteColumn
|
|
744
|
+
formula = $formula
|
|
745
|
+
}
|
|
746
|
+
$formulaEntries.Add($formulaEntry)
|
|
747
|
+
if (-not $detailed) {
|
|
748
|
+
$blockFormulas.Add([ordered]@{
|
|
749
|
+
rowOffset = $r - 1
|
|
750
|
+
columnOffset = $c - 1
|
|
751
|
+
formula = $formula
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if ($detailed) {
|
|
756
|
+
$entry = [ordered]@{
|
|
757
|
+
path = "/sheet[$([string]$sheet.Name)]/cell[$address]"
|
|
758
|
+
ref = $address
|
|
759
|
+
value = $value
|
|
760
|
+
formula = $formula
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if ($detailed -and $payload.includeStyles) {
|
|
764
|
+
$cell = $range.Cells.Item($r, $c)
|
|
765
|
+
$entry.style = [ordered]@{
|
|
766
|
+
fontName = [string]$cell.Font.Name
|
|
767
|
+
fontSize = [double]$cell.Font.Size
|
|
768
|
+
bold = [bool]$cell.Font.Bold
|
|
769
|
+
italic = [bool]$cell.Font.Italic
|
|
770
|
+
color = [double]$cell.Font.Color
|
|
771
|
+
fillColor = [double]$cell.Interior.Color
|
|
772
|
+
numberFormat = [string]$cell.NumberFormat
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if ($detailed) { $cells.Add($entry) }
|
|
776
|
+
}
|
|
777
|
+
if (-not $detailed) { $blockValues[$r - 1] = $line }
|
|
778
|
+
}
|
|
779
|
+
if (-not $detailed) {
|
|
780
|
+
[object[]]$blockFormulaOutput = @($blockFormulas)
|
|
781
|
+
$rowBlocks.Add([ordered]@{
|
|
782
|
+
startRow = $rangeStartRow
|
|
783
|
+
startColumn = $rangeStartColumn
|
|
784
|
+
rowCount = $rangeRows
|
|
785
|
+
columnCount = $rangeColumns
|
|
786
|
+
values = $blockValues
|
|
787
|
+
formulas = $blockFormulaOutput
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
$cursor += $take
|
|
791
|
+
$remaining -= $take
|
|
792
|
+
}
|
|
793
|
+
$lineage = @($formulaEntries | ForEach-Object {
|
|
794
|
+
[ordered]@{
|
|
795
|
+
path = "$($_.path)/lineage"
|
|
796
|
+
from = $_.path
|
|
797
|
+
formula = $_.formula
|
|
798
|
+
precedents = @(Excel-FormulaPrecedents ([string]$_.formula) ([string]$sheet.Name))
|
|
799
|
+
}
|
|
800
|
+
})
|
|
801
|
+
[object[]]$cellOutput = @()
|
|
802
|
+
[object[]]$rowBlockOutput = @()
|
|
803
|
+
if ($detailed) { $cellOutput = @($cells) } else { $rowBlockOutput = @($rowBlocks) }
|
|
804
|
+
return [ordered]@{
|
|
805
|
+
format = 'xlsx'
|
|
806
|
+
path = [string]$book.FullName
|
|
807
|
+
activeSheet = [string]$book.ActiveSheet.Name
|
|
808
|
+
sheetCount = [int]$book.Worksheets.Count
|
|
809
|
+
sheets = @([ordered]@{
|
|
810
|
+
path = "/sheet[$([string]$sheet.Name)]"
|
|
811
|
+
name = [string]$sheet.Name
|
|
812
|
+
rows = $rowCount
|
|
813
|
+
columns = $columnCount
|
|
814
|
+
address = [string]$base.Address($false, $false)
|
|
815
|
+
cellCount = $total
|
|
816
|
+
representation = $(if ($detailed) { 'cells' } else { 'row-blocks' })
|
|
817
|
+
cells = $cellOutput
|
|
818
|
+
rowBlocks = $rowBlockOutput
|
|
819
|
+
lineageCount = $lineage.Count
|
|
820
|
+
formulaLineage = $lineage
|
|
821
|
+
truncated = $cursor -lt $total
|
|
822
|
+
})
|
|
823
|
+
pagination = [ordered]@{
|
|
824
|
+
unit = 'cell'
|
|
825
|
+
scope = "$([string]$sheet.Name)!$([string]$base.Address($false, $false))"
|
|
826
|
+
offset = $offset
|
|
827
|
+
limit = $limit
|
|
828
|
+
returned = $populated
|
|
829
|
+
scanned = $cursor - $offset
|
|
830
|
+
total = $total
|
|
831
|
+
nextOffset = $(if ($cursor -lt $total) { $cursor } else { $null })
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function Snapshot-PowerPoint($presentation, $payload) {
|
|
837
|
+
$slides = @()
|
|
838
|
+
$slideOffset = if ($payload.paged) { [Math]::Max(0, [int]$payload.offset) } else { 0 }
|
|
839
|
+
$slideLimit = if ($payload.paged) { [Math]::Max(1, [int]$payload.limit) } else { [int]$presentation.Slides.Count }
|
|
840
|
+
$slideNumbers = if ($payload.pages) {
|
|
841
|
+
@($payload.pages | ForEach-Object { [int]$_ })
|
|
842
|
+
} else {
|
|
843
|
+
$end = [Math]::Min([int]$presentation.Slides.Count, $slideOffset + $slideLimit)
|
|
844
|
+
if ($end -le $slideOffset) { @() } else { @(($slideOffset + 1)..$end) }
|
|
845
|
+
}
|
|
846
|
+
foreach ($slideNumber in $slideNumbers) {
|
|
847
|
+
if ($slideNumber -lt 1 -or $slideNumber -gt $presentation.Slides.Count) { throw "PowerPoint slide out of range: $slideNumber" }
|
|
848
|
+
$slide = $presentation.Slides.Item($slideNumber)
|
|
849
|
+
$shapes = @()
|
|
850
|
+
for ($shapeIndex = 1; $shapeIndex -le $slide.Shapes.Count; $shapeIndex++) {
|
|
851
|
+
$shape = $slide.Shapes.Item($shapeIndex)
|
|
852
|
+
$text = $null
|
|
853
|
+
try {
|
|
854
|
+
if ($shape.HasTextFrame -and $shape.TextFrame.HasText) { $text = [string]$shape.TextFrame.TextRange.Text }
|
|
855
|
+
} catch {}
|
|
856
|
+
$placeholder = $null
|
|
857
|
+
try {
|
|
858
|
+
if ([int]$shape.Type -eq 14) {
|
|
859
|
+
$placeholder = [ordered]@{
|
|
860
|
+
type = [int]$shape.PlaceholderFormat.Type
|
|
861
|
+
index = [int]$shape.PlaceholderFormat.Index
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} catch {}
|
|
865
|
+
$group = $null
|
|
866
|
+
try {
|
|
867
|
+
if ([int]$shape.Type -eq 6) {
|
|
868
|
+
$items = @()
|
|
869
|
+
for ($groupIndex = 1; $groupIndex -le $shape.GroupItems.Count; $groupIndex++) {
|
|
870
|
+
$item = $shape.GroupItems.Item($groupIndex)
|
|
871
|
+
$items += [ordered]@{ index = $groupIndex; name = [string]$item.Name; type = [int]$item.Type }
|
|
872
|
+
}
|
|
873
|
+
$group = [ordered]@{ count = $items.Count; items = $items }
|
|
874
|
+
}
|
|
875
|
+
} catch {}
|
|
876
|
+
$crop = $null
|
|
877
|
+
try {
|
|
878
|
+
if ([int]$shape.Type -eq 13) {
|
|
879
|
+
$crop = [ordered]@{
|
|
880
|
+
left = [double]$shape.PictureFormat.CropLeft
|
|
881
|
+
right = [double]$shape.PictureFormat.CropRight
|
|
882
|
+
top = [double]$shape.PictureFormat.CropTop
|
|
883
|
+
bottom = [double]$shape.PictureFormat.CropBottom
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} catch {}
|
|
887
|
+
$table = $null
|
|
888
|
+
try {
|
|
889
|
+
if ($shape.HasTable) {
|
|
890
|
+
$rows = @()
|
|
891
|
+
for ($rowIndex = 1; $rowIndex -le $shape.Table.Rows.Count; $rowIndex++) {
|
|
892
|
+
$cells = @()
|
|
893
|
+
for ($columnIndex = 1; $columnIndex -le $shape.Table.Columns.Count; $columnIndex++) {
|
|
894
|
+
$cells += [string]$shape.Table.Cell($rowIndex, $columnIndex).Shape.TextFrame.TextRange.Text
|
|
895
|
+
}
|
|
896
|
+
$rows += ,$cells
|
|
897
|
+
}
|
|
898
|
+
$table = [ordered]@{
|
|
899
|
+
rows = [int]$shape.Table.Rows.Count
|
|
900
|
+
columns = [int]$shape.Table.Columns.Count
|
|
901
|
+
values = $rows
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch {}
|
|
905
|
+
$shapes += [ordered]@{
|
|
906
|
+
path = "/slide[$([int]$slide.SlideIndex)]/shape[$shapeIndex]"
|
|
907
|
+
index = $shapeIndex
|
|
908
|
+
name = [string]$shape.Name
|
|
909
|
+
type = [int]$shape.Type
|
|
910
|
+
text = $text
|
|
911
|
+
placeholder = $placeholder
|
|
912
|
+
group = $group
|
|
913
|
+
crop = $crop
|
|
914
|
+
table = $table
|
|
915
|
+
left = [double]$shape.Left
|
|
916
|
+
top = [double]$shape.Top
|
|
917
|
+
width = [double]$shape.Width
|
|
918
|
+
height = [double]$shape.Height
|
|
919
|
+
rotation = [double]$shape.Rotation
|
|
920
|
+
fillColor = $(try { [double]$shape.Fill.ForeColor.RGB } catch { $null })
|
|
921
|
+
fillTransparency = $(try { [double]$shape.Fill.Transparency } catch { $null })
|
|
922
|
+
lineColor = $(try { [double]$shape.Line.ForeColor.RGB } catch { $null })
|
|
923
|
+
lineTransparency = $(try { [double]$shape.Line.Transparency } catch { $null })
|
|
924
|
+
shadow = $(try {
|
|
925
|
+
[ordered]@{
|
|
926
|
+
visible = [int]$shape.Shadow.Visible
|
|
927
|
+
color = [double]$shape.Shadow.ForeColor.RGB
|
|
928
|
+
transparency = [double]$shape.Shadow.Transparency
|
|
929
|
+
blur = [double]$shape.Shadow.Blur
|
|
930
|
+
offsetX = [double]$shape.Shadow.OffsetX
|
|
931
|
+
offsetY = [double]$shape.Shadow.OffsetY
|
|
932
|
+
}
|
|
933
|
+
} catch { $null })
|
|
934
|
+
textFrame = $(try {
|
|
935
|
+
[ordered]@{
|
|
936
|
+
marginLeft = [double]$shape.TextFrame.MarginLeft
|
|
937
|
+
marginTop = [double]$shape.TextFrame.MarginTop
|
|
938
|
+
marginRight = [double]$shape.TextFrame.MarginRight
|
|
939
|
+
marginBottom = [double]$shape.TextFrame.MarginBottom
|
|
940
|
+
paragraphSpacing = [double]$shape.TextFrame.TextRange.ParagraphFormat.SpaceAfter
|
|
941
|
+
}
|
|
942
|
+
} catch { $null })
|
|
943
|
+
font = $(try {
|
|
944
|
+
[ordered]@{
|
|
945
|
+
name = [string]$shape.TextFrame.TextRange.Font.Name
|
|
946
|
+
size = [double]$shape.TextFrame.TextRange.Font.Size
|
|
947
|
+
bold = [int]$shape.TextFrame.TextRange.Font.Bold
|
|
948
|
+
italic = [int]$shape.TextFrame.TextRange.Font.Italic
|
|
949
|
+
color = [double]$shape.TextFrame.TextRange.Font.Color.RGB
|
|
950
|
+
}
|
|
951
|
+
} catch { $null })
|
|
952
|
+
chart = $(try {
|
|
953
|
+
if ($shape.HasChart) {
|
|
954
|
+
$chart = $shape.Chart
|
|
955
|
+
[ordered]@{
|
|
956
|
+
path = "/slide[$([int]$slide.SlideIndex)]/shape[$shapeIndex]/chart"
|
|
957
|
+
chartType = [int]$chart.ChartType
|
|
958
|
+
title = $(if ($chart.HasTitle) { [string]$chart.ChartTitle.Text } else { '' })
|
|
959
|
+
seriesCount = [int]$chart.SeriesCollection().Count
|
|
960
|
+
series = $(try {
|
|
961
|
+
$items = @()
|
|
962
|
+
for ($seriesIndex = 1; $seriesIndex -le $chart.SeriesCollection().Count; $seriesIndex++) {
|
|
963
|
+
$series = $chart.SeriesCollection().Item($seriesIndex)
|
|
964
|
+
$items += [ordered]@{
|
|
965
|
+
index = $seriesIndex
|
|
966
|
+
name = [string]$series.Name
|
|
967
|
+
formula = ''
|
|
968
|
+
chartType = $(try { [int]$series.ChartType } catch { $null })
|
|
969
|
+
axisGroup = $(try { [int]$series.AxisGroup } catch { $null })
|
|
970
|
+
trendlineCount = $(try { [int]$series.Trendlines().Count } catch { 0 })
|
|
971
|
+
hasErrorBars = $(try { [bool]$series.HasErrorBars } catch { $false })
|
|
972
|
+
hasDataLabels = [bool]$series.HasDataLabels
|
|
973
|
+
dataLabels = $(if ($series.HasDataLabels) {
|
|
974
|
+
[ordered]@{
|
|
975
|
+
showValue = [bool]$series.DataLabels().ShowValue
|
|
976
|
+
showCategoryName = [bool]$series.DataLabels().ShowCategoryName
|
|
977
|
+
numberFormat = [string]$series.DataLabels().NumberFormat
|
|
978
|
+
position = [int]$series.DataLabels().Position
|
|
979
|
+
}
|
|
980
|
+
} else { $null })
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
,$items
|
|
984
|
+
} catch { ,@() })
|
|
985
|
+
axes = $(try {
|
|
986
|
+
$axes = @()
|
|
987
|
+
foreach ($axisSpec in @(
|
|
988
|
+
[ordered]@{ name = 'category'; type = 1 },
|
|
989
|
+
[ordered]@{ name = 'value'; type = 2 }
|
|
990
|
+
)) {
|
|
991
|
+
$axis = $chart.Axes([int]$axisSpec.type, 1)
|
|
992
|
+
$axes += [ordered]@{
|
|
993
|
+
type = [string]$axisSpec.name
|
|
994
|
+
title = $(if ($axis.HasTitle) { [string]$axis.AxisTitle.Text } else { '' })
|
|
995
|
+
minimum = $(try { [double]$axis.MinimumScale } catch { $null })
|
|
996
|
+
maximum = $(try { [double]$axis.MaximumScale } catch { $null })
|
|
997
|
+
majorUnit = $(try { [double]$axis.MajorUnit } catch { $null })
|
|
998
|
+
numberFormat = $(try { [string]$axis.TickLabels.NumberFormat } catch { '' })
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
$axes
|
|
1002
|
+
} catch { @() })
|
|
1003
|
+
}
|
|
1004
|
+
} else { $null }
|
|
1005
|
+
} catch { $null })
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
$notes = $null
|
|
1009
|
+
try { $notes = [string]$slide.NotesPage.Shapes.Placeholders.Item(2).TextFrame.TextRange.Text } catch {}
|
|
1010
|
+
$comments = @()
|
|
1011
|
+
try {
|
|
1012
|
+
for ($commentIndex = 1; $commentIndex -le $slide.Comments.Count; $commentIndex++) {
|
|
1013
|
+
$comment = $slide.Comments.Item($commentIndex)
|
|
1014
|
+
$comments += [ordered]@{
|
|
1015
|
+
path = "/slide[$([int]$slide.SlideIndex)]/comment[$commentIndex]"
|
|
1016
|
+
index = $commentIndex
|
|
1017
|
+
author = [string]$comment.Author
|
|
1018
|
+
initials = [string]$comment.AuthorInitials
|
|
1019
|
+
text = [string]$comment.Text
|
|
1020
|
+
left = [double]$comment.Left
|
|
1021
|
+
top = [double]$comment.Top
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} catch {}
|
|
1025
|
+
$animations = @()
|
|
1026
|
+
try {
|
|
1027
|
+
$sequence = $slide.TimeLine.MainSequence
|
|
1028
|
+
for ($effectIndex = 1; $effectIndex -le $sequence.Count; $effectIndex++) {
|
|
1029
|
+
$effect = $sequence.Item($effectIndex)
|
|
1030
|
+
$animations += [ordered]@{
|
|
1031
|
+
index = $effectIndex
|
|
1032
|
+
shape = [string]$effect.Shape.Name
|
|
1033
|
+
effect = [int]$effect.EffectType
|
|
1034
|
+
trigger = [int]$effect.Timing.TriggerType
|
|
1035
|
+
duration = [double]$effect.Timing.Duration
|
|
1036
|
+
delay = [double]$effect.Timing.TriggerDelayTime
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
} catch {}
|
|
1040
|
+
$layoutName = $(try { [string]$slide.CustomLayout.Name } catch { '' })
|
|
1041
|
+
$layoutIndex = 0
|
|
1042
|
+
try {
|
|
1043
|
+
for ($candidateIndex = 1; $candidateIndex -le $presentation.SlideMaster.CustomLayouts.Count; $candidateIndex++) {
|
|
1044
|
+
if ([string]::Equals([string]$presentation.SlideMaster.CustomLayouts.Item($candidateIndex).Name, $layoutName, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
1045
|
+
$layoutIndex = $candidateIndex
|
|
1046
|
+
break
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
} catch {}
|
|
1050
|
+
$followMasterBackground = $(try { [bool]$slide.FollowMasterBackground } catch { $false })
|
|
1051
|
+
$backgroundSource = $(if ($followMasterBackground) { 'layout' } else { 'slide' })
|
|
1052
|
+
$backgroundColor = $null
|
|
1053
|
+
try {
|
|
1054
|
+
if ($followMasterBackground) {
|
|
1055
|
+
try {
|
|
1056
|
+
$backgroundColor = Color-Hex ([long]$slide.CustomLayout.Background.Fill.ForeColor.RGB)
|
|
1057
|
+
} catch {}
|
|
1058
|
+
if (-not $backgroundColor) {
|
|
1059
|
+
$backgroundSource = 'master'
|
|
1060
|
+
try { $backgroundColor = Color-Hex ([long]$presentation.SlideMaster.Background.Fill.ForeColor.RGB) } catch {}
|
|
1061
|
+
}
|
|
1062
|
+
} else {
|
|
1063
|
+
$backgroundColor = Color-Hex ([long]$slide.Background.Fill.ForeColor.RGB)
|
|
1064
|
+
}
|
|
1065
|
+
} catch {}
|
|
1066
|
+
$slides += [ordered]@{
|
|
1067
|
+
path = "/slide[$([int]$slide.SlideIndex)]"
|
|
1068
|
+
index = [int]$slide.SlideIndex
|
|
1069
|
+
layout = [ordered]@{ index = $layoutIndex; name = $layoutName }
|
|
1070
|
+
background = [ordered]@{
|
|
1071
|
+
followMaster = $followMasterBackground
|
|
1072
|
+
source = $backgroundSource
|
|
1073
|
+
color = $backgroundColor
|
|
1074
|
+
}
|
|
1075
|
+
shapes = $shapes
|
|
1076
|
+
notes = $notes
|
|
1077
|
+
comments = $comments
|
|
1078
|
+
animations = $animations
|
|
1079
|
+
transition = [ordered]@{
|
|
1080
|
+
effect = $(try { [int]$slide.SlideShowTransition.EntryEffect } catch { 0 })
|
|
1081
|
+
advanceOnTime = $(try { [bool]$slide.SlideShowTransition.AdvanceOnTime } catch { $false })
|
|
1082
|
+
advanceTime = $(try { [double]$slide.SlideShowTransition.AdvanceTime } catch { 0 })
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
$layouts = @()
|
|
1087
|
+
try {
|
|
1088
|
+
for ($layoutIndex = 1; $layoutIndex -le $presentation.SlideMaster.CustomLayouts.Count; $layoutIndex++) {
|
|
1089
|
+
$layout = $presentation.SlideMaster.CustomLayouts.Item($layoutIndex)
|
|
1090
|
+
$layouts += [ordered]@{
|
|
1091
|
+
path = "/layout[$layoutIndex]"
|
|
1092
|
+
index = $layoutIndex
|
|
1093
|
+
name = [string]$layout.Name
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
} catch {}
|
|
1097
|
+
$designs = @()
|
|
1098
|
+
try {
|
|
1099
|
+
for ($designIndex = 1; $designIndex -le $presentation.Designs.Count; $designIndex++) {
|
|
1100
|
+
$design = $presentation.Designs.Item($designIndex)
|
|
1101
|
+
$designs += [ordered]@{
|
|
1102
|
+
index = $designIndex
|
|
1103
|
+
name = [string]$design.Name
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
} catch {}
|
|
1107
|
+
return [ordered]@{
|
|
1108
|
+
format = 'pptx'
|
|
1109
|
+
path = [string]$presentation.FullName
|
|
1110
|
+
slideCount = $presentation.Slides.Count
|
|
1111
|
+
slideWidth = [double]$presentation.PageSetup.SlideWidth
|
|
1112
|
+
slideHeight = [double]$presentation.PageSetup.SlideHeight
|
|
1113
|
+
layoutCount = $layouts.Count
|
|
1114
|
+
layouts = $layouts
|
|
1115
|
+
designCount = $designs.Count
|
|
1116
|
+
designs = $designs
|
|
1117
|
+
slides = $slides
|
|
1118
|
+
theme = $(try { [string]$presentation.Designs.Item(1).Name } catch { '' })
|
|
1119
|
+
pagination = $(if ($payload.paged) {
|
|
1120
|
+
[ordered]@{
|
|
1121
|
+
unit = 'slide'
|
|
1122
|
+
offset = $slideOffset
|
|
1123
|
+
limit = $slideLimit
|
|
1124
|
+
returned = $slides.Count
|
|
1125
|
+
total = [int]$presentation.Slides.Count
|
|
1126
|
+
nextOffset = $(if (-not $payload.pages -and $slideOffset + $slides.Count -lt [int]$presentation.Slides.Count) { $slideOffset + $slides.Count } else { $null })
|
|
1127
|
+
}
|
|
1128
|
+
} else { $null })
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function Same-OfficePath([string]$left, [string]$right) {
|
|
1133
|
+
try {
|
|
1134
|
+
return [string]::Equals(
|
|
1135
|
+
[System.IO.Path]::GetFullPath($left),
|
|
1136
|
+
[System.IO.Path]::GetFullPath($right),
|
|
1137
|
+
[System.StringComparison]::OrdinalIgnoreCase
|
|
1138
|
+
)
|
|
1139
|
+
} catch {
|
|
1140
|
+
return $false
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function Snapshot-Selection($document, [string]$format) {
|
|
1145
|
+
try {
|
|
1146
|
+
switch ($format) {
|
|
1147
|
+
'docx' {
|
|
1148
|
+
$selection = $document.Application.Selection
|
|
1149
|
+
if ($null -eq $selection -or -not (Same-OfficePath ([string]$selection.Document.FullName) ([string]$document.FullName))) {
|
|
1150
|
+
return [ordered]@{ available = $false; reason = 'The active Word selection belongs to another document.' }
|
|
1151
|
+
}
|
|
1152
|
+
$range = $selection.Range
|
|
1153
|
+
$paragraph = [Math]::Max(1, [int]$document.Range(0, [Math]::Max(0, [int]$range.Start)).Paragraphs.Count)
|
|
1154
|
+
$target = "/body/p[$paragraph]"
|
|
1155
|
+
return [ordered]@{
|
|
1156
|
+
available = $true
|
|
1157
|
+
kind = $(if ([int]$range.Start -eq [int]$range.End) { 'insertion-point' } else { 'text' })
|
|
1158
|
+
target = $target
|
|
1159
|
+
key = "$target`:$([int]$range.Start)-$([int]$range.End)"
|
|
1160
|
+
start = [int]$range.Start
|
|
1161
|
+
end = [int]$range.End
|
|
1162
|
+
text = $([string]$range.Text).TrimEnd("`r", "`a")
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
'xlsx' {
|
|
1166
|
+
$selection = $document.Application.Selection
|
|
1167
|
+
$sheet = $selection.Parent
|
|
1168
|
+
if ($null -eq $sheet -or -not (Same-OfficePath ([string]$sheet.Parent.FullName) ([string]$document.FullName))) {
|
|
1169
|
+
return [ordered]@{ available = $false; reason = 'The active Excel selection belongs to another workbook.' }
|
|
1170
|
+
}
|
|
1171
|
+
$address = [string]$selection.Address($false, $false)
|
|
1172
|
+
$activeCell = [string]$document.Application.ActiveCell.Address($false, $false)
|
|
1173
|
+
$target = "/sheet[$([string]$sheet.Name)]/range[$address]"
|
|
1174
|
+
return [ordered]@{
|
|
1175
|
+
available = $true
|
|
1176
|
+
kind = 'range'
|
|
1177
|
+
target = $target
|
|
1178
|
+
key = "$([string]$sheet.Name)!$address@$activeCell"
|
|
1179
|
+
sheet = [string]$sheet.Name
|
|
1180
|
+
address = $address
|
|
1181
|
+
activeCell = $activeCell
|
|
1182
|
+
rows = [int]$selection.Rows.Count
|
|
1183
|
+
columns = [int]$selection.Columns.Count
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
'pptx' {
|
|
1187
|
+
$window = $document.Application.ActiveWindow
|
|
1188
|
+
if ($null -eq $window) { return [ordered]@{ available = $false; reason = 'PowerPoint has no active window.' } }
|
|
1189
|
+
$selection = $window.Selection
|
|
1190
|
+
$slideIndex = $(try { [int]$window.View.Slide.SlideIndex } catch { 0 })
|
|
1191
|
+
$selectionType = [int]$selection.Type
|
|
1192
|
+
$paths = @()
|
|
1193
|
+
if (@(2, 3) -contains $selectionType -and $slideIndex -gt 0) {
|
|
1194
|
+
$slide = $document.Slides.Item($slideIndex)
|
|
1195
|
+
foreach ($selectedShape in @($selection.ShapeRange)) {
|
|
1196
|
+
for ($shapeIndex = 1; $shapeIndex -le $slide.Shapes.Count; $shapeIndex++) {
|
|
1197
|
+
if ([int]$slide.Shapes.Item($shapeIndex).Id -eq [int]$selectedShape.Id) {
|
|
1198
|
+
$paths += "/slide[$slideIndex]/shape[$shapeIndex]"
|
|
1199
|
+
break
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
} elseif ($selectionType -eq 1) {
|
|
1204
|
+
foreach ($selectedSlide in @($selection.SlideRange)) { $paths += "/slide[$([int]$selectedSlide.SlideIndex)]" }
|
|
1205
|
+
} elseif ($slideIndex -gt 0) {
|
|
1206
|
+
$paths += "/slide[$slideIndex]"
|
|
1207
|
+
}
|
|
1208
|
+
return [ordered]@{
|
|
1209
|
+
available = $true
|
|
1210
|
+
kind = $(switch ($selectionType) { 1 { 'slides' } 2 { 'shapes' } 3 { 'text' } default { 'none' } })
|
|
1211
|
+
target = $(if ($paths.Count -gt 0) { [string]$paths[0] } else { '/' })
|
|
1212
|
+
targets = $paths
|
|
1213
|
+
key = "$selectionType`:$($paths -join ',')"
|
|
1214
|
+
slide = $slideIndex
|
|
1215
|
+
selectionType = $selectionType
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
} catch {
|
|
1220
|
+
return [ordered]@{ available = $false; reason = [string]$_.Exception.Message }
|
|
1221
|
+
}
|
|
1222
|
+
return [ordered]@{ available = $false; reason = 'Selection is unsupported for this format.' }
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function Snapshot-Document($document, [string]$format, $payload) {
|
|
1226
|
+
$value = switch ($format) {
|
|
1227
|
+
'docx' { Snapshot-Word $document $payload }
|
|
1228
|
+
'xlsx' {
|
|
1229
|
+
if ($payload.paged) {
|
|
1230
|
+
$sheet = $document.ActiveSheet
|
|
1231
|
+
if ($payload.sheet) { $sheet = $document.Worksheets.Item([string]$payload.sheet) }
|
|
1232
|
+
$range = $sheet.UsedRange
|
|
1233
|
+
if ($payload.range) { $range = $sheet.Range([string]$payload.range) }
|
|
1234
|
+
if ($payload.range -or [int64]$range.Rows.Count * [int64]$range.Columns.Count -gt 500 -or [int64]$payload.offset -gt 0) {
|
|
1235
|
+
Snapshot-ExcelPage $document $payload
|
|
1236
|
+
break
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
Snapshot-Excel $document $payload
|
|
1240
|
+
}
|
|
1241
|
+
'pptx' { Snapshot-PowerPoint $document $payload }
|
|
1242
|
+
}
|
|
1243
|
+
if ([bool]$payload.includeSelection) { $value['selection'] = Snapshot-Selection $document $format }
|
|
1244
|
+
return $value
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function Color-Value([string]$hex) {
|
|
1248
|
+
$clean = $hex.TrimStart('#')
|
|
1249
|
+
if ($clean.Length -ne 6) { throw "Invalid color '$hex'; expected RRGGBB" }
|
|
1250
|
+
$r = [Convert]::ToInt32($clean.Substring(0, 2), 16)
|
|
1251
|
+
$g = [Convert]::ToInt32($clean.Substring(2, 2), 16)
|
|
1252
|
+
$b = [Convert]::ToInt32($clean.Substring(4, 2), 16)
|
|
1253
|
+
return $r + (256 * $g) + (65536 * $b)
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function Color-Hex([long]$color) {
|
|
1257
|
+
$r = $color -band 255
|
|
1258
|
+
$g = ($color -shr 8) -band 255
|
|
1259
|
+
$b = ($color -shr 16) -band 255
|
|
1260
|
+
return ('{0:X2}{1:X2}{2:X2}' -f $r, $g, $b)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function Operation-Property($op, [string]$name, $fallback) {
|
|
1264
|
+
$direct = $op.PSObject.Properties[$name]
|
|
1265
|
+
if ($null -ne $direct -and $null -ne $direct.Value) { return $direct.Value }
|
|
1266
|
+
$properties = $op.PSObject.Properties['properties']
|
|
1267
|
+
if ($null -ne $properties -and $null -ne $properties.Value) {
|
|
1268
|
+
$nested = $properties.Value.PSObject.Properties[$name]
|
|
1269
|
+
if ($null -ne $nested -and $null -ne $nested.Value) { return $nested.Value }
|
|
1270
|
+
}
|
|
1271
|
+
return $fallback
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function Template-ValueMap($op) {
|
|
1275
|
+
$map = @{}
|
|
1276
|
+
$source = $op.tokens
|
|
1277
|
+
if ($null -eq $source) { throw 'fill_template requires tokens as an object' }
|
|
1278
|
+
if ($source -is [System.Collections.IDictionary]) {
|
|
1279
|
+
foreach ($key in $source.Keys) { $map[[string]$key] = $source[$key] }
|
|
1280
|
+
} else {
|
|
1281
|
+
foreach ($property in @($source.PSObject.Properties)) { $map[[string]$property.Name] = $property.Value }
|
|
1282
|
+
}
|
|
1283
|
+
return $map
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function Template-Matches([string]$text) {
|
|
1287
|
+
$matches = @()
|
|
1288
|
+
foreach ($match in [regex]::Matches($text, '\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}')) {
|
|
1289
|
+
$matches += [pscustomobject]@{ Raw = [string]$match.Value; Key = [string]$match.Groups[1].Value }
|
|
1290
|
+
}
|
|
1291
|
+
return $matches
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function Word-StoryRanges($doc) {
|
|
1295
|
+
$ranges = @()
|
|
1296
|
+
for ($storyType = 1; $storyType -le 17; $storyType++) {
|
|
1297
|
+
try {
|
|
1298
|
+
$current = $doc.StoryRanges.Item($storyType)
|
|
1299
|
+
while ($null -ne $current) {
|
|
1300
|
+
$ranges += $current.Duplicate
|
|
1301
|
+
try { $current = $current.NextStoryRange } catch { $current = $null }
|
|
1302
|
+
}
|
|
1303
|
+
} catch {}
|
|
1304
|
+
}
|
|
1305
|
+
return $ranges
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function Fill-WordTemplate($doc, $op) {
|
|
1309
|
+
$values = Template-ValueMap $op
|
|
1310
|
+
$filled = [ordered]@{}
|
|
1311
|
+
foreach ($range in @(Word-StoryRanges $doc)) {
|
|
1312
|
+
$text = [string]$range.Text
|
|
1313
|
+
$variants = @{}
|
|
1314
|
+
foreach ($match in @(Template-Matches $text)) { $variants[$match.Raw] = $match.Key }
|
|
1315
|
+
foreach ($raw in $variants.Keys) {
|
|
1316
|
+
$key = [string]$variants[$raw]
|
|
1317
|
+
if (-not $values.ContainsKey($key)) { continue }
|
|
1318
|
+
$count = [regex]::Matches($text, [regex]::Escape([string]$raw)).Count
|
|
1319
|
+
if ($count -eq 0) { continue }
|
|
1320
|
+
$search = $range.Duplicate
|
|
1321
|
+
$changed = $search.Find.Execute([string]$raw, $false, $false, $false, $false, $false, $true, 0, $false, [string]$values[$key], 2)
|
|
1322
|
+
if ($changed) { $filled[$key] = $(if ($filled.Contains($key)) { [int]$filled[$key] + $count } else { $count }) }
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
$remaining = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
1326
|
+
foreach ($range in @(Word-StoryRanges $doc)) {
|
|
1327
|
+
foreach ($match in @(Template-Matches ([string]$range.Text))) { $null = $remaining.Add([string]$match.Key) }
|
|
1328
|
+
}
|
|
1329
|
+
$unfilled = @($remaining) | Sort-Object
|
|
1330
|
+
if ([bool]$op.strict -and $unfilled.Count -gt 0) { throw "Unfilled template tokens: $($unfilled -join ', ')" }
|
|
1331
|
+
return [ordered]@{ op = 'fill_template'; changed = $filled.Count -gt 0; filled = $filled; unfilledTokens = $unfilled; strict = [bool]$op.strict }
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function PowerPoint-ShapeTextRanges($shape) {
|
|
1335
|
+
$ranges = @()
|
|
1336
|
+
try {
|
|
1337
|
+
if ($shape.HasTextFrame -and $shape.TextFrame.HasText) { $ranges += $shape.TextFrame.TextRange }
|
|
1338
|
+
} catch {}
|
|
1339
|
+
try {
|
|
1340
|
+
if ($shape.HasTable) {
|
|
1341
|
+
for ($row = 1; $row -le $shape.Table.Rows.Count; $row++) {
|
|
1342
|
+
for ($column = 1; $column -le $shape.Table.Columns.Count; $column++) {
|
|
1343
|
+
$ranges += $shape.Table.Cell($row, $column).Shape.TextFrame.TextRange
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
} catch {}
|
|
1348
|
+
try {
|
|
1349
|
+
if ([int]$shape.Type -eq 6) {
|
|
1350
|
+
for ($index = 1; $index -le $shape.GroupItems.Count; $index++) {
|
|
1351
|
+
$ranges += @(PowerPoint-ShapeTextRanges $shape.GroupItems.Item($index))
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} catch {}
|
|
1355
|
+
return $ranges
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function PowerPoint-TextRanges($presentation) {
|
|
1359
|
+
$ranges = @()
|
|
1360
|
+
foreach ($slide in @($presentation.Slides)) {
|
|
1361
|
+
foreach ($shape in @($slide.Shapes)) { $ranges += @(PowerPoint-ShapeTextRanges $shape) }
|
|
1362
|
+
try {
|
|
1363
|
+
foreach ($shape in @($slide.NotesPage.Shapes)) { $ranges += @(PowerPoint-ShapeTextRanges $shape) }
|
|
1364
|
+
} catch {}
|
|
1365
|
+
}
|
|
1366
|
+
return $ranges
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function Fill-PowerPointTemplate($presentation, $op) {
|
|
1370
|
+
$values = Template-ValueMap $op
|
|
1371
|
+
$filled = [ordered]@{}
|
|
1372
|
+
foreach ($range in @(PowerPoint-TextRanges $presentation)) {
|
|
1373
|
+
$text = [string]$range.Text
|
|
1374
|
+
$variants = @{}
|
|
1375
|
+
foreach ($match in @(Template-Matches $text)) { $variants[$match.Raw] = $match.Key }
|
|
1376
|
+
foreach ($raw in $variants.Keys) {
|
|
1377
|
+
$key = [string]$variants[$raw]
|
|
1378
|
+
if (-not $values.ContainsKey($key)) { continue }
|
|
1379
|
+
$expected = [regex]::Matches($text, [regex]::Escape([string]$raw)).Count
|
|
1380
|
+
$replaced = 0
|
|
1381
|
+
$after = 0
|
|
1382
|
+
while ($replaced -lt $expected) {
|
|
1383
|
+
$found = $range.Replace([string]$raw, [string]$values[$key], $after, 0, 0)
|
|
1384
|
+
if ($null -eq $found) { break }
|
|
1385
|
+
$replaced++
|
|
1386
|
+
$after = [int]$found.Start + [Math]::Max([int]$found.Length, 1) - 1
|
|
1387
|
+
}
|
|
1388
|
+
if ($replaced -gt 0) { $filled[$key] = $(if ($filled.Contains($key)) { [int]$filled[$key] + $replaced } else { $replaced }) }
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
$remaining = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
1392
|
+
foreach ($range in @(PowerPoint-TextRanges $presentation)) {
|
|
1393
|
+
foreach ($match in @(Template-Matches ([string]$range.Text))) { $null = $remaining.Add([string]$match.Key) }
|
|
1394
|
+
}
|
|
1395
|
+
$unfilled = @($remaining) | Sort-Object
|
|
1396
|
+
if ([bool]$op.strict -and $unfilled.Count -gt 0) { throw "Unfilled template tokens: $($unfilled -join ', ')" }
|
|
1397
|
+
return [ordered]@{ op = 'fill_template'; changed = $filled.Count -gt 0; filled = $filled; unfilledTokens = $unfilled; strict = [bool]$op.strict }
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function Word-StyleValue([string]$name) {
|
|
1401
|
+
$styles = @{
|
|
1402
|
+
normal = -1
|
|
1403
|
+
heading1 = -2
|
|
1404
|
+
heading2 = -3
|
|
1405
|
+
heading3 = -4
|
|
1406
|
+
heading4 = -5
|
|
1407
|
+
heading5 = -6
|
|
1408
|
+
heading6 = -7
|
|
1409
|
+
heading7 = -8
|
|
1410
|
+
heading8 = -9
|
|
1411
|
+
heading9 = -10
|
|
1412
|
+
title = -63
|
|
1413
|
+
subtitle = -75
|
|
1414
|
+
tablegrid = -155
|
|
1415
|
+
}
|
|
1416
|
+
$key = $name.Replace(' ', '').Replace('-', '').ToLowerInvariant()
|
|
1417
|
+
if ($styles.ContainsKey($key)) { return [int]$styles[$key] }
|
|
1418
|
+
return $name
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function Provenance-Text($op) {
|
|
1422
|
+
$source = $op.source
|
|
1423
|
+
if ($null -eq $source) { throw 'add_provenance requires source' }
|
|
1424
|
+
$document = if ($source -is [string]) { [string]$source } else { [string]$source.document }
|
|
1425
|
+
$target = if ($source -is [string]) { '' } else { [string]$source.target }
|
|
1426
|
+
$label = if ($source -is [string]) { '' } else { [string]$source.label }
|
|
1427
|
+
if ([string]::IsNullOrWhiteSpace($document)) { throw 'add_provenance source.document is required' }
|
|
1428
|
+
$text = "Source: $document"
|
|
1429
|
+
if (-not [string]::IsNullOrWhiteSpace($target)) { $text += "#$target" }
|
|
1430
|
+
if (-not [string]::IsNullOrWhiteSpace($label)) { $text += " ($label)" }
|
|
1431
|
+
return $text
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function Apply-WordOperation($doc, $op) {
|
|
1435
|
+
switch ([string]$op.op) {
|
|
1436
|
+
'fill_template' { return Fill-WordTemplate $doc $op }
|
|
1437
|
+
'replace_text' {
|
|
1438
|
+
$range = $doc.Content.Duplicate
|
|
1439
|
+
$ok = $range.Find.Execute([string]$op.find, $false, $false, $false, $false, $false, $true, 1, $false, [string]$op.replace, 2)
|
|
1440
|
+
return [ordered]@{ op = 'replace_text'; changed = [bool]$ok }
|
|
1441
|
+
}
|
|
1442
|
+
'append_text' {
|
|
1443
|
+
$text = [string]$op.text
|
|
1444
|
+
$existing = ([string]$doc.Content.Text).TrimEnd("`r", "`a")
|
|
1445
|
+
$reusedInitialParagraph = [string]::IsNullOrWhiteSpace($existing)
|
|
1446
|
+
if ($reusedInitialParagraph) {
|
|
1447
|
+
$paragraph = $doc.Paragraphs.Item(1)
|
|
1448
|
+
$paragraph.Range.Text = "$text`r"
|
|
1449
|
+
} else {
|
|
1450
|
+
$range = $doc.Content.Duplicate
|
|
1451
|
+
$range.Collapse(0)
|
|
1452
|
+
$paragraph = $doc.Paragraphs.Add($range)
|
|
1453
|
+
$paragraph.Range.Text = "$text`r"
|
|
1454
|
+
}
|
|
1455
|
+
$style = if ($op.style) { [string]$op.style } elseif ($op.properties.style) { [string]$op.properties.style } else { '' }
|
|
1456
|
+
if ($style) { $paragraph.Range.Style = Word-StyleValue $style }
|
|
1457
|
+
$props = $op.properties
|
|
1458
|
+
if ($props.name) { $paragraph.Range.Font.Name = [string]$props.name }
|
|
1459
|
+
if ($props.size) { $paragraph.Range.Font.Size = [single]$props.size }
|
|
1460
|
+
if ($null -ne $props.bold) { $paragraph.Range.Font.Bold = if ($props.bold) { -1 } else { 0 } }
|
|
1461
|
+
if ($null -ne $props.italic) { $paragraph.Range.Font.Italic = if ($props.italic) { -1 } else { 0 } }
|
|
1462
|
+
if ($props.color) { $paragraph.Range.Font.Color = Color-Value ([string]$props.color) }
|
|
1463
|
+
$format = $paragraph.Format
|
|
1464
|
+
if ($props.alignment) {
|
|
1465
|
+
$format.Alignment = switch ([string]$props.alignment) { 'center' { 1 } 'right' { 2 } 'justify' { 3 } default { 0 } }
|
|
1466
|
+
}
|
|
1467
|
+
if ($null -ne $props.spacingBefore) { $format.SpaceBefore = [single]$props.spacingBefore }
|
|
1468
|
+
if ($null -ne $props.spacingAfter) { $format.SpaceAfter = [single]$props.spacingAfter }
|
|
1469
|
+
if ($null -ne $props.lineSpacing) { $format.LineSpacing = [single]$props.lineSpacing }
|
|
1470
|
+
if ($null -ne $props.keepWithNext) { $format.KeepWithNext = if ($props.keepWithNext) { -1 } else { 0 } }
|
|
1471
|
+
if ($null -ne $props.pageBreakBefore) { $format.PageBreakBefore = if ($props.pageBreakBefore) { -1 } else { 0 } }
|
|
1472
|
+
if ($props.tabStops) {
|
|
1473
|
+
$format.TabStops.ClearAll()
|
|
1474
|
+
foreach ($tab in @($props.tabStops)) {
|
|
1475
|
+
$alignment = switch ([string]$tab.alignment) { 'center' { 1 } 'right' { 2 } 'decimal' { 3 } 'bar' { 4 } default { 0 } }
|
|
1476
|
+
$leader = switch ([string]$tab.leader) { 'dot' { 1 } 'dash' { 2 } 'line' { 3 } 'heavy' { 4 } 'middleDot' { 5 } default { 0 } }
|
|
1477
|
+
$null = $format.TabStops.Add([single]$tab.position, $alignment, $leader)
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if ($props.border) {
|
|
1481
|
+
$borderIndex = switch ([string]$props.border.side) { 'top' { -1 } 'left' { -2 } 'right' { -4 } default { -3 } }
|
|
1482
|
+
$border = $paragraph.Borders.Item($borderIndex)
|
|
1483
|
+
$border.LineStyle = 1
|
|
1484
|
+
if ($props.border.color) { $border.Color = Color-Value ([string]$props.border.color) }
|
|
1485
|
+
}
|
|
1486
|
+
if ($props.listKind) {
|
|
1487
|
+
$kind = ([string]$props.listKind).ToLowerInvariant()
|
|
1488
|
+
if ($kind -eq 'number') {
|
|
1489
|
+
$paragraph.Range.ListFormat.ApplyNumberDefault()
|
|
1490
|
+
} elseif ($kind -ne 'none') {
|
|
1491
|
+
$paragraph.Range.ListFormat.ApplyBulletDefault()
|
|
1492
|
+
} else {
|
|
1493
|
+
$paragraph.Range.ListFormat.RemoveNumbers()
|
|
1494
|
+
}
|
|
1495
|
+
for ($level = 0; $level -lt [int]$props.listLevel; $level++) { $paragraph.Range.ListFormat.ListIndent() }
|
|
1496
|
+
} else {
|
|
1497
|
+
$paragraph.Range.ListFormat.RemoveNumbers()
|
|
1498
|
+
}
|
|
1499
|
+
$paragraphIndex = if ($reusedInitialParagraph) { 1 } else { [Math]::Max(1, [int]$doc.Paragraphs.Count - 1) }
|
|
1500
|
+
return [ordered]@{ op = 'append_text'; changed = $true; paragraph = $paragraphIndex; path = "/body/p[$paragraphIndex]"; style = $style }
|
|
1501
|
+
}
|
|
1502
|
+
'set_table_cell' {
|
|
1503
|
+
$doc.Tables.Item([int]$op.table).Cell([int]$op.row, [int]$op.col).Range.Text = [string]$op.text
|
|
1504
|
+
return [ordered]@{ op = 'set_table_cell'; changed = $true }
|
|
1505
|
+
}
|
|
1506
|
+
'add_table' {
|
|
1507
|
+
$values = @($op.values)
|
|
1508
|
+
$rows = if ($op.rows) { [int]$op.rows } elseif ($values.Count -gt 0) { [int]$values.Count } else { 1 }
|
|
1509
|
+
$columns = if ($op.columns) {
|
|
1510
|
+
[int]$op.columns
|
|
1511
|
+
} elseif ($values.Count -gt 0) {
|
|
1512
|
+
[Math]::Max(1, [int]@($values | ForEach-Object { @($_).Count } | Measure-Object -Maximum).Maximum)
|
|
1513
|
+
} else {
|
|
1514
|
+
1
|
|
1515
|
+
}
|
|
1516
|
+
if ($op.paragraph) {
|
|
1517
|
+
$range = $doc.Paragraphs.Item([int]$op.paragraph).Range.Duplicate
|
|
1518
|
+
$range.Collapse(0)
|
|
1519
|
+
} else {
|
|
1520
|
+
$range = $doc.Content.Duplicate
|
|
1521
|
+
$range.Collapse(0)
|
|
1522
|
+
}
|
|
1523
|
+
$table = $doc.Tables.Add($range, $rows, $columns)
|
|
1524
|
+
for ($row = 1; $row -le $rows; $row++) {
|
|
1525
|
+
for ($column = 1; $column -le $columns; $column++) {
|
|
1526
|
+
if ($row -le $values.Count -and $column -le @($values[$row - 1]).Count) {
|
|
1527
|
+
$table.Cell($row, $column).Range.Text = [string]@($values[$row - 1])[$column - 1]
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
$props = $op.properties
|
|
1532
|
+
if ($props.style) { $table.Style = Word-StyleValue ([string]$props.style) }
|
|
1533
|
+
if ($props.textStyle) { $table.Range.Style = Word-StyleValue ([string]$props.textStyle) }
|
|
1534
|
+
if ($props.fontName) { $table.Range.Font.Name = [string]$props.fontName }
|
|
1535
|
+
if ($props.fontSize) { $table.Range.Font.Size = [single]$props.fontSize }
|
|
1536
|
+
if ($props.color) { $table.Range.Font.Color = Color-Value ([string]$props.color) }
|
|
1537
|
+
if ($null -ne $props.spacingAfter) { $table.Range.ParagraphFormat.SpaceAfter = [single]$props.spacingAfter }
|
|
1538
|
+
if ($props.alignment) {
|
|
1539
|
+
$table.Rows.Alignment = switch ([string]$props.alignment) { 'center' { 1 } 'right' { 2 } default { 0 } }
|
|
1540
|
+
}
|
|
1541
|
+
if ($props.columnWidths) {
|
|
1542
|
+
for ($column = 1; $column -le [Math]::Min($columns, @($props.columnWidths).Count); $column++) {
|
|
1543
|
+
$table.Columns.Item($column).Width = [single]@($props.columnWidths)[$column - 1]
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if ($props.borders) { $table.Borders.Enable = 1 }
|
|
1547
|
+
if ($props.shading) { $table.Shading.BackgroundPatternColor = Color-Value ([string]$props.shading) }
|
|
1548
|
+
return [ordered]@{ op = 'add_table'; changed = $true; table = [int]$table.Index; rows = $rows; columns = $columns }
|
|
1549
|
+
}
|
|
1550
|
+
'set_table_style' {
|
|
1551
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1552
|
+
$props = $op.properties
|
|
1553
|
+
if ($props.style) { $table.Style = Word-StyleValue ([string]$props.style) }
|
|
1554
|
+
if ($props.alignment) {
|
|
1555
|
+
$table.Rows.Alignment = switch ([string]$props.alignment) { 'center' { 1 } 'right' { 2 } default { 0 } }
|
|
1556
|
+
}
|
|
1557
|
+
if ($props.columnWidths) {
|
|
1558
|
+
for ($column = 1; $column -le [Math]::Min($table.Columns.Count, @($props.columnWidths).Count); $column++) {
|
|
1559
|
+
$table.Columns.Item($column).Width = [single]@($props.columnWidths)[$column - 1]
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if ($props.borders) { $table.Borders.Enable = 1 }
|
|
1563
|
+
if ($props.shading) { $table.Shading.BackgroundPatternColor = Color-Value ([string]$props.shading) }
|
|
1564
|
+
return [ordered]@{ op = 'set_table_style'; changed = $true; table = [int]$op.table }
|
|
1565
|
+
}
|
|
1566
|
+
'merge_table_cells' {
|
|
1567
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1568
|
+
$rowSpan = [Math]::Max(1, $(if ($op.rowSpan) { [int]$op.rowSpan } else { 1 }))
|
|
1569
|
+
$colSpan = [Math]::Max(1, $(if ($op.colSpan) { [int]$op.colSpan } else { 1 }))
|
|
1570
|
+
$lastRow = [int]$op.row + $rowSpan - 1
|
|
1571
|
+
$lastCol = [int]$op.col + $colSpan - 1
|
|
1572
|
+
$table.Cell([int]$op.row, [int]$op.col).Merge($table.Cell($lastRow, $lastCol))
|
|
1573
|
+
return [ordered]@{ op = 'merge_table_cells'; changed = $true; table = [int]$op.table; row = [int]$op.row; col = [int]$op.col }
|
|
1574
|
+
}
|
|
1575
|
+
'set_table_cell_style' {
|
|
1576
|
+
$cell = $doc.Tables.Item([int]$op.table).Cell([int]$op.row, [int]$op.col)
|
|
1577
|
+
$props = $op.properties
|
|
1578
|
+
if ($props.fillColor) { $cell.Shading.BackgroundPatternColor = Color-Value ([string]$props.fillColor) }
|
|
1579
|
+
if ($props.verticalAlignment) {
|
|
1580
|
+
$cell.VerticalAlignment = switch ([string]$props.verticalAlignment) { 'center' { 1 } 'bottom' { 3 } default { 0 } }
|
|
1581
|
+
}
|
|
1582
|
+
if ($props.width) { $cell.Width = [single]$props.width }
|
|
1583
|
+
if ($null -ne $props.bold) { $cell.Range.Font.Bold = if ($props.bold) { -1 } else { 0 } }
|
|
1584
|
+
if ($null -ne $props.italic) { $cell.Range.Font.Italic = if ($props.italic) { -1 } else { 0 } }
|
|
1585
|
+
if ($props.fontName) { $cell.Range.Font.Name = [string]$props.fontName }
|
|
1586
|
+
if ($props.fontSize) { $cell.Range.Font.Size = [single]$props.fontSize }
|
|
1587
|
+
if ($props.color) { $cell.Range.Font.Color = Color-Value ([string]$props.color) }
|
|
1588
|
+
return [ordered]@{ op = 'set_table_cell_style'; changed = $true; table = [int]$op.table; row = [int]$op.row; col = [int]$op.col }
|
|
1589
|
+
}
|
|
1590
|
+
'set_paragraph_text' {
|
|
1591
|
+
$paragraph = $doc.Paragraphs.Item([int]$op.paragraph)
|
|
1592
|
+
$paragraph.Range.Text = ([string]$op.text) + "`r"
|
|
1593
|
+
return [ordered]@{ op = 'set_paragraph_text'; changed = $true }
|
|
1594
|
+
}
|
|
1595
|
+
'set_run_text' {
|
|
1596
|
+
$paragraph = $doc.Paragraphs.Item([int]$op.paragraph)
|
|
1597
|
+
$run = $paragraph.Range.Words.Item([int]$op.run)
|
|
1598
|
+
$run.Text = [string]$op.text
|
|
1599
|
+
return [ordered]@{ op = 'set_run_text'; changed = $true }
|
|
1600
|
+
}
|
|
1601
|
+
'remove_paragraph' {
|
|
1602
|
+
$doc.Paragraphs.Item([int]$op.paragraph).Range.Delete()
|
|
1603
|
+
return [ordered]@{ op = 'remove_paragraph'; changed = $true }
|
|
1604
|
+
}
|
|
1605
|
+
'move_paragraph' {
|
|
1606
|
+
$source = $doc.Paragraphs.Item([int]$op.paragraph).Range.Duplicate
|
|
1607
|
+
$formatted = $source.FormattedText
|
|
1608
|
+
$source.Delete()
|
|
1609
|
+
$destination = $doc.Paragraphs.Item([int]$op.index).Range.Duplicate
|
|
1610
|
+
$destination.Collapse(1)
|
|
1611
|
+
$destination.FormattedText = $formatted
|
|
1612
|
+
return [ordered]@{ op = 'move_paragraph'; changed = $true }
|
|
1613
|
+
}
|
|
1614
|
+
'add_image' {
|
|
1615
|
+
$width = if ($op.width) { [single]$op.width } else { [single]0 }
|
|
1616
|
+
$height = if ($op.height) { [single]$op.height } else { [single]0 }
|
|
1617
|
+
$shape = $doc.InlineShapes.AddPicture([string]$op.path, $false, $true)
|
|
1618
|
+
if ($width -gt 0) { $shape.Width = $width }
|
|
1619
|
+
if ($height -gt 0) { $shape.Height = $height }
|
|
1620
|
+
return [ordered]@{ op = 'add_image'; changed = $true }
|
|
1621
|
+
}
|
|
1622
|
+
'add_comment' {
|
|
1623
|
+
$range = $doc.Content.Duplicate
|
|
1624
|
+
$found = $range.Find.Execute([string]$op.find)
|
|
1625
|
+
if (-not $found) { throw "Comment target not found: $($op.find)" }
|
|
1626
|
+
$null = $doc.Comments.Add($range, [string]$op.text)
|
|
1627
|
+
return [ordered]@{ op = 'add_comment'; changed = $true }
|
|
1628
|
+
}
|
|
1629
|
+
'add_comment_reply' {
|
|
1630
|
+
$index = [int]$op.comment
|
|
1631
|
+
if ($index -lt 1 -or $index -gt $doc.Comments.Count) { throw "Comment index out of range: $index" }
|
|
1632
|
+
$comment = $doc.Comments.Item($index)
|
|
1633
|
+
try {
|
|
1634
|
+
$range = $comment.Scope.Duplicate
|
|
1635
|
+
$null = $comment.Replies.Add($range, [string]$op.text)
|
|
1636
|
+
} catch {
|
|
1637
|
+
throw 'This Word version does not expose threaded comment replies through COM'
|
|
1638
|
+
}
|
|
1639
|
+
return [ordered]@{ op = 'add_comment_reply'; changed = $true; comment = $index }
|
|
1640
|
+
}
|
|
1641
|
+
'add_provenance' {
|
|
1642
|
+
$paragraphIndex = [int]$op.paragraph
|
|
1643
|
+
if ($paragraphIndex -lt 1 -or $paragraphIndex -gt $doc.Paragraphs.Count) { throw "Provenance paragraph index out of range: $paragraphIndex" }
|
|
1644
|
+
$range = $doc.Paragraphs.Item($paragraphIndex).Range.Duplicate
|
|
1645
|
+
if ($range.End -gt $range.Start) { $range.End-- }
|
|
1646
|
+
$text = Provenance-Text $op
|
|
1647
|
+
$null = $doc.Comments.Add($range, $text)
|
|
1648
|
+
return [ordered]@{ op = 'add_provenance'; changed = $true; target = "/body/p[$paragraphIndex]"; citation = $text }
|
|
1649
|
+
}
|
|
1650
|
+
'delete_comment' {
|
|
1651
|
+
$index = [int]$op.comment
|
|
1652
|
+
if ($index -lt 1 -or $index -gt $doc.Comments.Count) { throw "Comment index out of range: $index" }
|
|
1653
|
+
$comment = $doc.Comments.Item($index)
|
|
1654
|
+
if ($comment.Replies.Count -gt 0) {
|
|
1655
|
+
$comment.DeleteRecursively()
|
|
1656
|
+
} else {
|
|
1657
|
+
$comment.Delete()
|
|
1658
|
+
}
|
|
1659
|
+
return [ordered]@{ op = 'delete_comment'; changed = $true; comment = $index }
|
|
1660
|
+
}
|
|
1661
|
+
'set_comment_resolved' {
|
|
1662
|
+
$index = [int]$op.comment
|
|
1663
|
+
if ($index -lt 1 -or $index -gt $doc.Comments.Count) { throw "Comment index out of range: $index" }
|
|
1664
|
+
$comment = $doc.Comments.Item($index)
|
|
1665
|
+
try {
|
|
1666
|
+
$comment.Done = [bool]$op.resolved
|
|
1667
|
+
} catch {
|
|
1668
|
+
throw 'This Word version does not expose resolved comment state through COM'
|
|
1669
|
+
}
|
|
1670
|
+
return [ordered]@{ op = 'set_comment_resolved'; changed = $true; comment = $index; resolved = [bool]$op.resolved }
|
|
1671
|
+
}
|
|
1672
|
+
'set_font' {
|
|
1673
|
+
$range = $doc.Content.Duplicate
|
|
1674
|
+
if ($op.find -and -not $range.Find.Execute([string]$op.find)) { throw "Font target not found: $($op.find)" }
|
|
1675
|
+
$props = $op.properties
|
|
1676
|
+
if ($props.name) { $range.Font.Name = [string]$props.name }
|
|
1677
|
+
if ($props.size) { $range.Font.Size = [single]$props.size }
|
|
1678
|
+
if ($null -ne $props.bold) { $range.Font.Bold = if ($props.bold) { -1 } else { 0 } }
|
|
1679
|
+
if ($null -ne $props.italic) { $range.Font.Italic = if ($props.italic) { -1 } else { 0 } }
|
|
1680
|
+
if ($props.color) { $range.Font.Color = Color-Value ([string]$props.color) }
|
|
1681
|
+
return [ordered]@{ op = 'set_font'; changed = $true }
|
|
1682
|
+
}
|
|
1683
|
+
'set_paragraph_style' {
|
|
1684
|
+
$paragraph = $doc.Paragraphs.Item([int]$op.paragraph)
|
|
1685
|
+
$paragraph.Range.Style = Word-StyleValue ([string]$op.style)
|
|
1686
|
+
return [ordered]@{ op = 'set_paragraph_style'; changed = $true; style = [string]$op.style }
|
|
1687
|
+
}
|
|
1688
|
+
'set_paragraph_format' {
|
|
1689
|
+
$paragraph = $doc.Paragraphs.Item([int]$op.paragraph)
|
|
1690
|
+
$format = $paragraph.Format
|
|
1691
|
+
$props = $op.properties
|
|
1692
|
+
if ($props.alignment) {
|
|
1693
|
+
$format.Alignment = switch ([string]$props.alignment) { 'center' { 1 } 'right' { 2 } 'justify' { 3 } default { 0 } }
|
|
1694
|
+
}
|
|
1695
|
+
if ($null -ne $props.spacingBefore) { $format.SpaceBefore = [single]$props.spacingBefore }
|
|
1696
|
+
if ($null -ne $props.spacingAfter) { $format.SpaceAfter = [single]$props.spacingAfter }
|
|
1697
|
+
if ($null -ne $props.lineSpacing) { $format.LineSpacing = [single]$props.lineSpacing }
|
|
1698
|
+
if ($null -ne $props.keepWithNext) { $format.KeepWithNext = if ($props.keepWithNext) { -1 } else { 0 } }
|
|
1699
|
+
if ($null -ne $props.pageBreakBefore) { $format.PageBreakBefore = if ($props.pageBreakBefore) { -1 } else { 0 } }
|
|
1700
|
+
if ($props.tabStops) {
|
|
1701
|
+
$format.TabStops.ClearAll()
|
|
1702
|
+
foreach ($tab in @($props.tabStops)) {
|
|
1703
|
+
$alignment = switch ([string]$tab.alignment) { 'center' { 1 } 'right' { 2 } 'decimal' { 3 } 'bar' { 4 } default { 0 } }
|
|
1704
|
+
$leader = switch ([string]$tab.leader) { 'dot' { 1 } 'dash' { 2 } 'line' { 3 } 'heavy' { 4 } 'middleDot' { 5 } default { 0 } }
|
|
1705
|
+
$null = $format.TabStops.Add([single]$tab.position, $alignment, $leader)
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if ($props.border) {
|
|
1709
|
+
$borderIndex = switch ([string]$props.border.side) { 'top' { -1 } 'left' { -2 } 'right' { -4 } default { -3 } }
|
|
1710
|
+
$border = $paragraph.Borders.Item($borderIndex)
|
|
1711
|
+
$border.LineStyle = 1
|
|
1712
|
+
if ($props.border.color) { $border.Color = Color-Value ([string]$props.border.color) }
|
|
1713
|
+
}
|
|
1714
|
+
return [ordered]@{ op = 'set_paragraph_format'; changed = $true; paragraph = [int]$op.paragraph }
|
|
1715
|
+
}
|
|
1716
|
+
'insert_table_row' {
|
|
1717
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1718
|
+
if ($op.row) { $null = $table.Rows.Add($table.Rows.Item([int]$op.row)) } else { $null = $table.Rows.Add() }
|
|
1719
|
+
return [ordered]@{ op = 'insert_table_row'; changed = $true; rows = $table.Rows.Count }
|
|
1720
|
+
}
|
|
1721
|
+
'delete_table_row' {
|
|
1722
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1723
|
+
$table.Rows.Item([int]$op.row).Delete()
|
|
1724
|
+
return [ordered]@{ op = 'delete_table_row'; changed = $true; rows = $table.Rows.Count }
|
|
1725
|
+
}
|
|
1726
|
+
'insert_table_column' {
|
|
1727
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1728
|
+
if ($op.column) { $null = $table.Columns.Add($table.Columns.Item([int]$op.column)) } else { $null = $table.Columns.Add() }
|
|
1729
|
+
return [ordered]@{ op = 'insert_table_column'; changed = $true; columns = $table.Columns.Count }
|
|
1730
|
+
}
|
|
1731
|
+
'delete_table_column' {
|
|
1732
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1733
|
+
$table.Columns.Item([int]$op.column).Delete()
|
|
1734
|
+
return [ordered]@{ op = 'delete_table_column'; changed = $true; columns = $table.Columns.Count }
|
|
1735
|
+
}
|
|
1736
|
+
'set_header_footer' {
|
|
1737
|
+
$section = $doc.Sections.Item($(if ($op.section) { [int]$op.section } else { 1 }))
|
|
1738
|
+
$kind = switch ([string]$op.kind) {
|
|
1739
|
+
'first' { 2 }
|
|
1740
|
+
'even' { 3 }
|
|
1741
|
+
default { 1 }
|
|
1742
|
+
}
|
|
1743
|
+
$collection = if ($null -ne $op.header -and -not [bool]$op.header) { $section.Footers } else { $section.Headers }
|
|
1744
|
+
$collection.Item($kind).Range.Text = [string]$op.text
|
|
1745
|
+
return [ordered]@{ op = 'set_header_footer'; changed = $true; section = $section.Index }
|
|
1746
|
+
}
|
|
1747
|
+
'track_changes' {
|
|
1748
|
+
$doc.TrackRevisions = [bool]$op.enabled
|
|
1749
|
+
return [ordered]@{ op = 'track_changes'; changed = $true; enabled = [bool]$doc.TrackRevisions }
|
|
1750
|
+
}
|
|
1751
|
+
'resolve_revisions' {
|
|
1752
|
+
$resolution = ([string]$op.resolution).ToLowerInvariant()
|
|
1753
|
+
$count = $doc.Revisions.Count
|
|
1754
|
+
if ($resolution -eq 'reject') { $doc.RejectAllRevisions() } else { $doc.AcceptAllRevisions() }
|
|
1755
|
+
return [ordered]@{ op = 'resolve_revisions'; changed = $count -gt 0; resolved = $count; resolution = $(if ($resolution -eq 'reject') { 'reject' } else { 'accept' }) }
|
|
1756
|
+
}
|
|
1757
|
+
'resolve_revision' {
|
|
1758
|
+
$index = [int]$op.revision
|
|
1759
|
+
if ($index -lt 1 -or $index -gt $doc.Revisions.Count) { throw "Revision index out of range: $index" }
|
|
1760
|
+
$resolution = ([string]$op.resolution).ToLowerInvariant()
|
|
1761
|
+
$revision = $doc.Revisions.Item($index)
|
|
1762
|
+
if ($resolution -eq 'reject') { $revision.Reject() } else { $revision.Accept() }
|
|
1763
|
+
return [ordered]@{ op = 'resolve_revision'; changed = $true; revision = $index; resolution = $(if ($resolution -eq 'reject') { 'reject' } else { 'accept' }) }
|
|
1764
|
+
}
|
|
1765
|
+
'insert_toc' {
|
|
1766
|
+
$range = if ($op.paragraph) {
|
|
1767
|
+
$doc.Paragraphs.Item([int]$op.paragraph).Range.Duplicate
|
|
1768
|
+
} else {
|
|
1769
|
+
$doc.Range(0, 0)
|
|
1770
|
+
}
|
|
1771
|
+
$range.Collapse(1)
|
|
1772
|
+
$toc = $doc.TablesOfContents.Add(
|
|
1773
|
+
$range,
|
|
1774
|
+
$true,
|
|
1775
|
+
$(if ($op.lowerHeadingLevel) { [int]$op.lowerHeadingLevel } else { 1 }),
|
|
1776
|
+
$(if ($op.upperHeadingLevel) { [int]$op.upperHeadingLevel } else { 9 })
|
|
1777
|
+
)
|
|
1778
|
+
try { $toc.Update() } catch {}
|
|
1779
|
+
return [ordered]@{ op = 'insert_toc'; changed = $true; index = [int]$toc.Index }
|
|
1780
|
+
}
|
|
1781
|
+
'add_page_numbers' {
|
|
1782
|
+
$section = $doc.Sections.Item($(if ($op.section) { [int]$op.section } else { 1 }))
|
|
1783
|
+
$kind = switch ([string]$op.kind) {
|
|
1784
|
+
'first' { 2 }
|
|
1785
|
+
'even' { 3 }
|
|
1786
|
+
default { 1 }
|
|
1787
|
+
}
|
|
1788
|
+
$footer = $section.Footers.Item($kind)
|
|
1789
|
+
$range = $footer.Range
|
|
1790
|
+
$range.Text = $(if ($null -ne $op.prefix) { [string]$op.prefix } else { 'Page ' })
|
|
1791
|
+
$range = $footer.Range
|
|
1792
|
+
$range.Collapse(0)
|
|
1793
|
+
$null = $footer.Range.Fields.Add($range, -1, 'PAGE', $true)
|
|
1794
|
+
if ($null -eq $op.includeTotal -or [bool]$op.includeTotal) {
|
|
1795
|
+
$range = $footer.Range
|
|
1796
|
+
$range.Collapse(0)
|
|
1797
|
+
$range.InsertAfter($(if ($null -ne $op.separator) { [string]$op.separator } else { ' of ' }))
|
|
1798
|
+
$range = $footer.Range
|
|
1799
|
+
$range.Collapse(0)
|
|
1800
|
+
$null = $footer.Range.Fields.Add($range, -1, 'NUMPAGES', $true)
|
|
1801
|
+
}
|
|
1802
|
+
return [ordered]@{ op = 'add_page_numbers'; changed = $true; section = [int]$section.Index }
|
|
1803
|
+
}
|
|
1804
|
+
'insert_break' {
|
|
1805
|
+
$range = if ($op.paragraph) {
|
|
1806
|
+
$doc.Paragraphs.Item([int]$op.paragraph).Range.Duplicate
|
|
1807
|
+
} else {
|
|
1808
|
+
$doc.Content.Duplicate
|
|
1809
|
+
}
|
|
1810
|
+
$range.Collapse(0)
|
|
1811
|
+
$breakType = switch ([string]$op.kind) {
|
|
1812
|
+
'section_next' { 2 }
|
|
1813
|
+
'section_continuous' { 3 }
|
|
1814
|
+
default { 7 }
|
|
1815
|
+
}
|
|
1816
|
+
$range.InsertBreak($breakType)
|
|
1817
|
+
return [ordered]@{ op = 'insert_break'; changed = $true; kind = $(if ($op.kind) { [string]$op.kind } else { 'page' }) }
|
|
1818
|
+
}
|
|
1819
|
+
'set_list' {
|
|
1820
|
+
$paragraph = $doc.Paragraphs.Item([int]$op.paragraph)
|
|
1821
|
+
$kind = ([string]$op.kind).ToLowerInvariant()
|
|
1822
|
+
if ($kind -eq 'none') {
|
|
1823
|
+
$paragraph.Range.ListFormat.RemoveNumbers()
|
|
1824
|
+
} elseif ($kind -eq 'number') {
|
|
1825
|
+
$paragraph.Range.ListFormat.ApplyNumberDefault()
|
|
1826
|
+
} else {
|
|
1827
|
+
$paragraph.Range.ListFormat.ApplyBulletDefault()
|
|
1828
|
+
}
|
|
1829
|
+
if ($op.level) { $paragraph.Range.ListFormat.ListIndent(); for ($level = 2; $level -lt [int]$op.level; $level++) { $paragraph.Range.ListFormat.ListIndent() } }
|
|
1830
|
+
return [ordered]@{ op = 'set_list'; changed = $true; paragraph = [int]$op.paragraph; kind = $(if ($kind) { $kind } else { 'bullet' }) }
|
|
1831
|
+
}
|
|
1832
|
+
'add_hyperlink' {
|
|
1833
|
+
$range = if ($op.paragraph) { $doc.Paragraphs.Item([int]$op.paragraph).Range.Duplicate } else { $doc.Content.Duplicate }
|
|
1834
|
+
if ($op.find) {
|
|
1835
|
+
$found = $range.Find.Execute([string]$op.find, $false, $false, $false, $false, $false, $true)
|
|
1836
|
+
if (-not $found) { throw "Hyperlink target text not found: $($op.find)" }
|
|
1837
|
+
} elseif (-not $op.paragraph) {
|
|
1838
|
+
$range.Collapse(0)
|
|
1839
|
+
}
|
|
1840
|
+
$display = if ($null -ne $op.display) { [string]$op.display } else { [string]$range.Text }
|
|
1841
|
+
$link = $doc.Hyperlinks.Add($range, [string]$op.address, [string]$op.subAddress, $null, $display)
|
|
1842
|
+
return [ordered]@{ op = 'add_hyperlink'; changed = $true; address = [string]$link.Address; subAddress = [string]$link.SubAddress }
|
|
1843
|
+
}
|
|
1844
|
+
'add_bookmark' {
|
|
1845
|
+
$range = if ($op.paragraph) { $doc.Paragraphs.Item([int]$op.paragraph).Range.Duplicate } else { $doc.Content.Duplicate }
|
|
1846
|
+
if ($op.find) {
|
|
1847
|
+
$found = $range.Find.Execute([string]$op.find, $false, $false, $false, $false, $false, $true)
|
|
1848
|
+
if (-not $found) { throw "Bookmark target text not found: $($op.find)" }
|
|
1849
|
+
} elseif (-not $op.paragraph) {
|
|
1850
|
+
$range.Collapse(0)
|
|
1851
|
+
}
|
|
1852
|
+
$bookmark = $doc.Bookmarks.Add([string]$op.name, $range)
|
|
1853
|
+
return [ordered]@{ op = 'add_bookmark'; changed = $true; name = [string]$bookmark.Name }
|
|
1854
|
+
}
|
|
1855
|
+
'set_page' {
|
|
1856
|
+
$section = $doc.Sections.Item($(if ($op.section) { [int]$op.section } else { 1 }))
|
|
1857
|
+
$props = $op.properties
|
|
1858
|
+
if ($props.orientation) { $section.PageSetup.Orientation = $(if ([string]$props.orientation -eq 'landscape') { 1 } else { 0 }) }
|
|
1859
|
+
if ($props.topMargin) { $section.PageSetup.TopMargin = [single]$props.topMargin }
|
|
1860
|
+
if ($props.bottomMargin) { $section.PageSetup.BottomMargin = [single]$props.bottomMargin }
|
|
1861
|
+
if ($props.leftMargin) { $section.PageSetup.LeftMargin = [single]$props.leftMargin }
|
|
1862
|
+
if ($props.rightMargin) { $section.PageSetup.RightMargin = [single]$props.rightMargin }
|
|
1863
|
+
return [ordered]@{ op = 'set_page'; changed = $true; section = $section.Index }
|
|
1864
|
+
}
|
|
1865
|
+
'fit_table' {
|
|
1866
|
+
$table = $doc.Tables.Item([int]$op.table)
|
|
1867
|
+
$table.AutoFitBehavior(2)
|
|
1868
|
+
return [ordered]@{ op = 'fit_table'; changed = $true; table = [int]$op.table }
|
|
1869
|
+
}
|
|
1870
|
+
default { throw "Unsupported DOCX operation: $($op.op)" }
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function Excel-Sheet($book, $op) {
|
|
1875
|
+
if ($op.sheet) { return $book.Worksheets.Item([string]$op.sheet) }
|
|
1876
|
+
return $book.ActiveSheet
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function Excel-CellValue($value) {
|
|
1880
|
+
if ($null -eq $value) { return $null }
|
|
1881
|
+
if ($value -is [System.Boolean]) { return [bool]$value }
|
|
1882
|
+
if (
|
|
1883
|
+
$value -is [System.Byte] -or
|
|
1884
|
+
$value -is [System.SByte] -or
|
|
1885
|
+
$value -is [System.Int16] -or
|
|
1886
|
+
$value -is [System.UInt16] -or
|
|
1887
|
+
$value -is [System.Int32] -or
|
|
1888
|
+
$value -is [System.UInt32] -or
|
|
1889
|
+
$value -is [System.Int64] -or
|
|
1890
|
+
$value -is [System.UInt64] -or
|
|
1891
|
+
$value -is [System.Single] -or
|
|
1892
|
+
$value -is [System.Double] -or
|
|
1893
|
+
$value -is [System.Decimal]
|
|
1894
|
+
) {
|
|
1895
|
+
return [double]$value
|
|
1896
|
+
}
|
|
1897
|
+
return [string]$value
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function Set-ExcelCellValue($cell, $value) {
|
|
1901
|
+
$converted = Excel-CellValue $value
|
|
1902
|
+
if ($null -eq $converted) {
|
|
1903
|
+
$cell.ClearContents()
|
|
1904
|
+
return
|
|
1905
|
+
}
|
|
1906
|
+
$arguments = [object[]]::new(1)
|
|
1907
|
+
$arguments[0] = $converted
|
|
1908
|
+
$cell.GetType().InvokeMember(
|
|
1909
|
+
'Value2',
|
|
1910
|
+
[System.Reflection.BindingFlags]::SetProperty,
|
|
1911
|
+
$null,
|
|
1912
|
+
$cell,
|
|
1913
|
+
$arguments
|
|
1914
|
+
) | Out-Null
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function Excel-PivotField($pivot, [string]$name) {
|
|
1918
|
+
$collection = $pivot.PivotFields()
|
|
1919
|
+
$available = @()
|
|
1920
|
+
for ($index = 1; $index -le $collection.Count; $index++) {
|
|
1921
|
+
$candidate = $collection.Item($index)
|
|
1922
|
+
$candidateName = [string]$candidate.Name
|
|
1923
|
+
$available += $candidateName
|
|
1924
|
+
if ([string]::Equals($candidateName, $name, [System.StringComparison]::OrdinalIgnoreCase)) { return $candidate }
|
|
1925
|
+
}
|
|
1926
|
+
throw "Pivot field '$name' not found; available fields: $($available -join ', ')"
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function PowerPoint-Layout($presentation, $reference) {
|
|
1930
|
+
$layouts = $presentation.SlideMaster.CustomLayouts
|
|
1931
|
+
if ($null -eq $reference -or [string]::IsNullOrWhiteSpace([string]$reference)) {
|
|
1932
|
+
for ($index = 1; $index -le $layouts.Count; $index++) {
|
|
1933
|
+
if ([string]::Equals([string]$layouts.Item($index).Name, 'Blank', [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
1934
|
+
return [ordered]@{ Index = $index; Layout = $layouts.Item($index) }
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
for ($index = 1; $index -le $layouts.Count; $index++) {
|
|
1938
|
+
$candidate = $layouts.Item($index)
|
|
1939
|
+
$contentPlaceholderCount = 0
|
|
1940
|
+
for ($shapeIndex = 1; $shapeIndex -le $candidate.Shapes.Count; $shapeIndex++) {
|
|
1941
|
+
try {
|
|
1942
|
+
$shape = $candidate.Shapes.Item($shapeIndex)
|
|
1943
|
+
if ([int]$shape.Type -ne 14) { continue }
|
|
1944
|
+
$placeholderType = [int]$shape.PlaceholderFormat.Type
|
|
1945
|
+
if ($placeholderType -notin @(13, 15, 16)) { $contentPlaceholderCount++ }
|
|
1946
|
+
} catch {
|
|
1947
|
+
$contentPlaceholderCount++
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if ($contentPlaceholderCount -eq 0) {
|
|
1951
|
+
return [ordered]@{ Index = $index; Layout = $candidate }
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return [ordered]@{ Index = $layouts.Count; Layout = $layouts.Item($layouts.Count) }
|
|
1955
|
+
}
|
|
1956
|
+
$numeric = 0
|
|
1957
|
+
if ([int]::TryParse([string]$reference, [ref]$numeric)) {
|
|
1958
|
+
if ($numeric -lt 1 -or $numeric -gt $layouts.Count) { throw "PowerPoint layout index out of range: $numeric" }
|
|
1959
|
+
return [ordered]@{ Index = $numeric; Layout = $layouts.Item($numeric) }
|
|
1960
|
+
}
|
|
1961
|
+
$available = @()
|
|
1962
|
+
for ($index = 1; $index -le $layouts.Count; $index++) {
|
|
1963
|
+
$candidate = $layouts.Item($index)
|
|
1964
|
+
$name = [string]$candidate.Name
|
|
1965
|
+
$available += $name
|
|
1966
|
+
if ([string]::Equals($name, [string]$reference, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
1967
|
+
return [ordered]@{ Index = $index; Layout = $candidate }
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
throw "PowerPoint layout '$reference' not found; available layouts: $($available -join ', ')"
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function Apply-ExcelOperation($book, $op) {
|
|
1974
|
+
switch ([string]$op.op) {
|
|
1975
|
+
'replace_text' {
|
|
1976
|
+
$changed = $false
|
|
1977
|
+
foreach ($sheet in @($book.Worksheets)) {
|
|
1978
|
+
$changed = [bool]$sheet.Cells.Replace([string]$op.find, [string]$op.replace, 2, 1, $false, $false, $false, $false) -or $changed
|
|
1979
|
+
}
|
|
1980
|
+
return [ordered]@{ op = 'replace_text'; changed = $changed }
|
|
1981
|
+
}
|
|
1982
|
+
'set_cell' {
|
|
1983
|
+
$sheet = Excel-Sheet $book $op
|
|
1984
|
+
$target = $sheet.Range([string]$op.cell)
|
|
1985
|
+
Set-ExcelCellValue $target $op.value
|
|
1986
|
+
return [ordered]@{ op = 'set_cell'; changed = $true }
|
|
1987
|
+
}
|
|
1988
|
+
'set_formula' {
|
|
1989
|
+
$sheet = Excel-Sheet $book $op
|
|
1990
|
+
$formula = [string]$op.formula
|
|
1991
|
+
if (-not $formula.StartsWith('=')) { $formula = "=$formula" }
|
|
1992
|
+
$sheet.Range([string]$op.cell).Formula = $formula
|
|
1993
|
+
return [ordered]@{ op = 'set_formula'; changed = $true }
|
|
1994
|
+
}
|
|
1995
|
+
'set_range' {
|
|
1996
|
+
$sheet = Excel-Sheet $book $op
|
|
1997
|
+
$target = $sheet.Range([string]$op.range)
|
|
1998
|
+
$rows = @($op.values)
|
|
1999
|
+
$rowCount = $rows.Count
|
|
2000
|
+
$columnCount = if ($rowCount -gt 0) { @($rows[0]).Count } else { 0 }
|
|
2001
|
+
if ($rowCount -eq [int]$target.Rows.Count -and $columnCount -eq [int]$target.Columns.Count -and ($rowCount * $columnCount) -gt 500) {
|
|
2002
|
+
$matrix = New-Object 'object[,]' $rowCount, $columnCount
|
|
2003
|
+
for ($r = 0; $r -lt $rowCount; $r++) {
|
|
2004
|
+
$line = @($rows[$r])
|
|
2005
|
+
for ($c = 0; $c -lt $columnCount; $c++) { $matrix[$r, $c] = $line[$c] }
|
|
2006
|
+
}
|
|
2007
|
+
$target.Value2 = $matrix
|
|
2008
|
+
return [ordered]@{ op = 'set_range'; changed = $true; bulk = $true; cells = $rowCount * $columnCount }
|
|
2009
|
+
}
|
|
2010
|
+
for ($r = 0; $r -lt $rows.Count; $r++) {
|
|
2011
|
+
$line = @($rows[$r])
|
|
2012
|
+
for ($c = 0; $c -lt $line.Count; $c++) {
|
|
2013
|
+
$cell = $target.Cells.Item($r + 1, $c + 1)
|
|
2014
|
+
Set-ExcelCellValue $cell $line[$c]
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return [ordered]@{ op = 'set_range'; changed = $true }
|
|
2018
|
+
}
|
|
2019
|
+
'append_row' {
|
|
2020
|
+
$sheet = Excel-Sheet $book $op
|
|
2021
|
+
$row = [int]$sheet.UsedRange.Row + [int]$sheet.UsedRange.Rows.Count
|
|
2022
|
+
$values = @($op.values)
|
|
2023
|
+
for ($c = 0; $c -lt $values.Count; $c++) {
|
|
2024
|
+
$cell = $sheet.Cells.Item($row, $c + 1)
|
|
2025
|
+
Set-ExcelCellValue $cell $values[$c]
|
|
2026
|
+
}
|
|
2027
|
+
return [ordered]@{ op = 'append_row'; changed = $true; row = $row }
|
|
2028
|
+
}
|
|
2029
|
+
'add_sheet' {
|
|
2030
|
+
$sheet = $book.Worksheets.Add()
|
|
2031
|
+
$sheet.Name = [string]$op.name
|
|
2032
|
+
return [ordered]@{ op = 'add_sheet'; changed = $true; sheet = [string]$sheet.Name }
|
|
2033
|
+
}
|
|
2034
|
+
'copy_sheet' {
|
|
2035
|
+
$source = $book.Worksheets.Item([string]$op.sheet)
|
|
2036
|
+
$source.Copy($null, $book.Worksheets.Item($book.Worksheets.Count))
|
|
2037
|
+
$copy = $book.Worksheets.Item($book.Worksheets.Count)
|
|
2038
|
+
if ($op.name) { $copy.Name = [string]$op.name }
|
|
2039
|
+
return [ordered]@{ op = 'copy_sheet'; changed = $true; sheet = [string]$copy.Name }
|
|
2040
|
+
}
|
|
2041
|
+
'clear_cell' {
|
|
2042
|
+
$sheet = Excel-Sheet $book $op
|
|
2043
|
+
$sheet.Range([string]$op.cell).Clear()
|
|
2044
|
+
return [ordered]@{ op = 'clear_cell'; changed = $true }
|
|
2045
|
+
}
|
|
2046
|
+
'add_note' {
|
|
2047
|
+
$sheet = Excel-Sheet $book $op
|
|
2048
|
+
$cell = $sheet.Range([string]$op.cell)
|
|
2049
|
+
if ($null -ne $cell.Comment) { $cell.Comment.Delete() }
|
|
2050
|
+
$comment = $cell.AddComment([string]$op.text)
|
|
2051
|
+
return [ordered]@{ op = 'add_note'; changed = $true; cell = [string]$cell.Address($false, $false); author = [string]$comment.Author }
|
|
2052
|
+
}
|
|
2053
|
+
'add_provenance' {
|
|
2054
|
+
$sheet = Excel-Sheet $book $op
|
|
2055
|
+
$cell = $sheet.Range([string]$op.cell)
|
|
2056
|
+
$text = Provenance-Text $op
|
|
2057
|
+
if ($null -ne $cell.Comment) {
|
|
2058
|
+
$existing = [string]$cell.Comment.Text()
|
|
2059
|
+
if ($existing -notmatch [regex]::Escape($text)) { $cell.Comment.Text("$existing`n$text") }
|
|
2060
|
+
} else {
|
|
2061
|
+
$null = $cell.AddComment($text)
|
|
2062
|
+
}
|
|
2063
|
+
return [ordered]@{ op = 'add_provenance'; changed = $true; target = "/sheet[$([string]$sheet.Name)]/cell[$([string]$cell.Address($false, $false))]"; citation = $text }
|
|
2064
|
+
}
|
|
2065
|
+
'delete_note' {
|
|
2066
|
+
$sheet = Excel-Sheet $book $op
|
|
2067
|
+
$cell = $sheet.Range([string]$op.cell)
|
|
2068
|
+
$changed = $null -ne $cell.Comment
|
|
2069
|
+
if ($changed) { $cell.Comment.Delete() }
|
|
2070
|
+
return [ordered]@{ op = 'delete_note'; changed = $changed; cell = [string]$cell.Address($false, $false) }
|
|
2071
|
+
}
|
|
2072
|
+
'delete_sheet' {
|
|
2073
|
+
$book.Worksheets.Item([string]$op.sheet).Delete()
|
|
2074
|
+
return [ordered]@{ op = 'delete_sheet'; changed = $true }
|
|
2075
|
+
}
|
|
2076
|
+
'set_style' {
|
|
2077
|
+
$sheet = Excel-Sheet $book $op
|
|
2078
|
+
$target = $sheet.Range([string]$(if ($op.range) { $op.range } else { $op.cell }))
|
|
2079
|
+
$props = $op.properties
|
|
2080
|
+
if ($props.fontName) { $target.Font.Name = [string]$props.fontName }
|
|
2081
|
+
if ($props.fontSize) { $target.Font.Size = [single]$props.fontSize }
|
|
2082
|
+
if ($null -ne $props.bold) { $target.Font.Bold = [bool]$props.bold }
|
|
2083
|
+
if ($null -ne $props.italic) { $target.Font.Italic = [bool]$props.italic }
|
|
2084
|
+
if ($props.numberFormat) { $target.NumberFormat = [string]$props.numberFormat }
|
|
2085
|
+
if ($props.color) { $target.Font.Color = Color-Value ([string]$props.color) }
|
|
2086
|
+
if ($props.fillColor) { $target.Interior.Color = Color-Value ([string]$props.fillColor) }
|
|
2087
|
+
if ($props.horizontalAlignment) {
|
|
2088
|
+
$target.HorizontalAlignment = switch ([string]$props.horizontalAlignment) {
|
|
2089
|
+
'center' { -4108 }
|
|
2090
|
+
'right' { -4152 }
|
|
2091
|
+
'justify' { -4130 }
|
|
2092
|
+
default { -4131 }
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if ($props.verticalAlignment) {
|
|
2096
|
+
$target.VerticalAlignment = switch ([string]$props.verticalAlignment) {
|
|
2097
|
+
'center' { -4108 }
|
|
2098
|
+
'bottom' { -4107 }
|
|
2099
|
+
default { -4160 }
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if ($null -ne $props.wrapText) { $target.WrapText = [bool]$props.wrapText }
|
|
2103
|
+
return [ordered]@{ op = 'set_style'; changed = $true }
|
|
2104
|
+
}
|
|
2105
|
+
'add_image' {
|
|
2106
|
+
$sheet = Excel-Sheet $book $op
|
|
2107
|
+
$left = if ($op.left) { [single]$op.left } else { [single]0 }
|
|
2108
|
+
$top = if ($op.top) { [single]$op.top } else { [single]0 }
|
|
2109
|
+
$width = if ($op.width) { [single]$op.width } else { [single]320 }
|
|
2110
|
+
$height = if ($op.height) { [single]$op.height } else { [single]240 }
|
|
2111
|
+
$null = $sheet.Shapes.AddPicture([string]$op.path, $false, $true, $left, $top, $width, $height)
|
|
2112
|
+
return [ordered]@{ op = 'add_image'; changed = $true }
|
|
2113
|
+
}
|
|
2114
|
+
'rename_sheet' {
|
|
2115
|
+
$sheet = Excel-Sheet $book $op
|
|
2116
|
+
$sheet.Name = [string]$op.name
|
|
2117
|
+
return [ordered]@{ op = 'rename_sheet'; changed = $true; sheet = [string]$sheet.Name }
|
|
2118
|
+
}
|
|
2119
|
+
'add_table' {
|
|
2120
|
+
$sheet = Excel-Sheet $book $op
|
|
2121
|
+
$source = $sheet.Range([string]$op.range)
|
|
2122
|
+
$table = $sheet.ListObjects.Add(1, $source, $null, 1)
|
|
2123
|
+
if ($op.name) { $table.Name = [string]$op.name }
|
|
2124
|
+
if ($op.style) { $table.TableStyle = [string]$op.style }
|
|
2125
|
+
return [ordered]@{ op = 'add_table'; changed = $true; name = [string]$table.Name }
|
|
2126
|
+
}
|
|
2127
|
+
'add_chart' {
|
|
2128
|
+
$sheet = Excel-Sheet $book $op
|
|
2129
|
+
$chartTypes = @{ column = 51; bar = 57; line = 4; pie = 5; area = 1; scatter = -4169 }
|
|
2130
|
+
$kind = ([string]$op.chartType).ToLowerInvariant()
|
|
2131
|
+
$chartType = if ($kind -and $chartTypes.ContainsKey($kind)) { [int]$chartTypes[$kind] } elseif ($op.chartType -as [int]) { [int]$op.chartType } else { 51 }
|
|
2132
|
+
$left = if ($null -ne $op.left) { [single]$op.left } else { [single]300 }
|
|
2133
|
+
$top = if ($null -ne $op.top) { [single]$op.top } else { [single]20 }
|
|
2134
|
+
$width = if ($op.width) { [single]$op.width } else { [single]480 }
|
|
2135
|
+
$height = if ($op.height) { [single]$op.height } else { [single]280 }
|
|
2136
|
+
$shape = $sheet.Shapes.AddChart2(-1, $chartType, $left, $top, $width, $height)
|
|
2137
|
+
if ($op.range) { $shape.Chart.SetSourceData($sheet.Range([string]$op.range), 2) }
|
|
2138
|
+
if ($op.title) { $shape.Chart.HasTitle = $true; $shape.Chart.ChartTitle.Text = [string]$op.title }
|
|
2139
|
+
$seriesCount = [int]$shape.Chart.SeriesCollection().Count
|
|
2140
|
+
$pointCount = if ($seriesCount -gt 0) { [int]$shape.Chart.SeriesCollection().Item(1).Points().Count } else { 0 }
|
|
2141
|
+
return [ordered]@{ op = 'add_chart'; changed = $true; name = [string]$shape.Name; series = $seriesCount; categories = $pointCount }
|
|
2142
|
+
}
|
|
2143
|
+
'add_conditional_format' {
|
|
2144
|
+
$sheet = Excel-Sheet $book $op
|
|
2145
|
+
$target = $sheet.Range([string]$op.range)
|
|
2146
|
+
if ($op.formula) {
|
|
2147
|
+
$rule = $target.FormatConditions.Add(2, $null, [string]$op.formula)
|
|
2148
|
+
if ($op.color) { $rule.Font.Color = Color-Value ([string]$op.color) }
|
|
2149
|
+
if ($op.fillColor) { $rule.Interior.Color = Color-Value ([string]$op.fillColor) }
|
|
2150
|
+
} else {
|
|
2151
|
+
$null = $target.FormatConditions.AddColorScale(3)
|
|
2152
|
+
}
|
|
2153
|
+
return [ordered]@{ op = 'add_conditional_format'; changed = $true }
|
|
2154
|
+
}
|
|
2155
|
+
'delete_conditional_formats' {
|
|
2156
|
+
$sheet = Excel-Sheet $book $op
|
|
2157
|
+
$target = $sheet.Range([string]$op.range)
|
|
2158
|
+
$count = [int]$target.FormatConditions.Count
|
|
2159
|
+
$target.FormatConditions.Delete()
|
|
2160
|
+
return [ordered]@{ op = 'delete_conditional_formats'; changed = $count -gt 0; count = $count }
|
|
2161
|
+
}
|
|
2162
|
+
'add_validation' {
|
|
2163
|
+
$sheet = Excel-Sheet $book $op
|
|
2164
|
+
$target = $sheet.Range([string]$op.range)
|
|
2165
|
+
$target.Validation.Delete()
|
|
2166
|
+
$target.Validation.Add(3, 1, 1, [string]$op.formula1)
|
|
2167
|
+
if ($op.inputMessage) { $target.Validation.InputMessage = [string]$op.inputMessage }
|
|
2168
|
+
if ($op.errorMessage) { $target.Validation.ErrorMessage = [string]$op.errorMessage }
|
|
2169
|
+
return [ordered]@{ op = 'add_validation'; changed = $true }
|
|
2170
|
+
}
|
|
2171
|
+
'freeze_panes' {
|
|
2172
|
+
$sheet = Excel-Sheet $book $op
|
|
2173
|
+
$sheet.Activate()
|
|
2174
|
+
$window = $book.Windows.Item(1)
|
|
2175
|
+
$window.FreezePanes = $false
|
|
2176
|
+
$window.SplitRow = $(if ($null -ne $op.row) { [int]$op.row } else { 1 })
|
|
2177
|
+
$window.SplitColumn = $(if ($null -ne $op.column) { [int]$op.column } else { 0 })
|
|
2178
|
+
$window.FreezePanes = $true
|
|
2179
|
+
return [ordered]@{ op = 'freeze_panes'; changed = $true }
|
|
2180
|
+
}
|
|
2181
|
+
'add_pivot_table' {
|
|
2182
|
+
$sourceSheet = Excel-Sheet $book $op
|
|
2183
|
+
$destinationSheet = if ($op.destinationSheet) { $book.Worksheets.Item([string]$op.destinationSheet) } else { $sourceSheet }
|
|
2184
|
+
$cache = $book.PivotCaches().Create(1, $sourceSheet.Range([string]$op.source))
|
|
2185
|
+
$pivot = $cache.CreatePivotTable($destinationSheet.Range([string]$op.destination), [string]$(if ($op.name) { $op.name } else { "MixdogPivot$($destinationSheet.PivotTables().Count + 1)" }))
|
|
2186
|
+
foreach ($field in @($op.rows)) {
|
|
2187
|
+
if (-not [string]::IsNullOrWhiteSpace([string]$field)) { (Excel-PivotField $pivot ([string]$field)).Orientation = 1 }
|
|
2188
|
+
}
|
|
2189
|
+
foreach ($field in @($op.columns)) {
|
|
2190
|
+
if (-not [string]::IsNullOrWhiteSpace([string]$field)) { (Excel-PivotField $pivot ([string]$field)).Orientation = 2 }
|
|
2191
|
+
}
|
|
2192
|
+
foreach ($field in @($op.values)) {
|
|
2193
|
+
if ([string]::IsNullOrWhiteSpace([string]$field)) { continue }
|
|
2194
|
+
$dataField = $pivot.AddDataField((Excel-PivotField $pivot ([string]$field)), "Sum of $field", -4157)
|
|
2195
|
+
$null = $dataField
|
|
2196
|
+
}
|
|
2197
|
+
return [ordered]@{ op = 'add_pivot_table'; changed = $true; name = [string]$pivot.Name }
|
|
2198
|
+
}
|
|
2199
|
+
'insert_rows' {
|
|
2200
|
+
$sheet = Excel-Sheet $book $op
|
|
2201
|
+
$count = if ($op.count) { [int]$op.count } else { 1 }
|
|
2202
|
+
for ($index = 0; $index -lt $count; $index++) { $sheet.Rows.Item([int]$op.row).Insert(-4121) }
|
|
2203
|
+
return [ordered]@{ op = 'insert_rows'; changed = $true; row = [int]$op.row; count = $count; referenceAware = $true }
|
|
2204
|
+
}
|
|
2205
|
+
'delete_rows' {
|
|
2206
|
+
$sheet = Excel-Sheet $book $op
|
|
2207
|
+
$count = if ($op.count) { [int]$op.count } else { 1 }
|
|
2208
|
+
for ($index = 0; $index -lt $count; $index++) { $sheet.Rows.Item([int]$op.row).Delete() }
|
|
2209
|
+
return [ordered]@{ op = 'delete_rows'; changed = $true; row = [int]$op.row; count = $count; referenceAware = $true }
|
|
2210
|
+
}
|
|
2211
|
+
'insert_columns' {
|
|
2212
|
+
$sheet = Excel-Sheet $book $op
|
|
2213
|
+
$count = if ($op.count) { [int]$op.count } else { 1 }
|
|
2214
|
+
for ($index = 0; $index -lt $count; $index++) { $sheet.Columns.Item([int]$op.column).Insert(-4161) }
|
|
2215
|
+
return [ordered]@{ op = 'insert_columns'; changed = $true; column = [int]$op.column; count = $count; referenceAware = $true }
|
|
2216
|
+
}
|
|
2217
|
+
'delete_columns' {
|
|
2218
|
+
$sheet = Excel-Sheet $book $op
|
|
2219
|
+
$count = if ($op.count) { [int]$op.count } else { 1 }
|
|
2220
|
+
for ($index = 0; $index -lt $count; $index++) { $sheet.Columns.Item([int]$op.column).Delete() }
|
|
2221
|
+
return [ordered]@{ op = 'delete_columns'; changed = $true; column = [int]$op.column; count = $count; referenceAware = $true }
|
|
2222
|
+
}
|
|
2223
|
+
'merge_cells' {
|
|
2224
|
+
$sheet = Excel-Sheet $book $op
|
|
2225
|
+
$sheet.Range([string]$op.range).Merge()
|
|
2226
|
+
return [ordered]@{ op = 'merge_cells'; changed = $true; range = [string]$op.range }
|
|
2227
|
+
}
|
|
2228
|
+
'unmerge_cells' {
|
|
2229
|
+
$sheet = Excel-Sheet $book $op
|
|
2230
|
+
$sheet.Range([string]$op.range).UnMerge()
|
|
2231
|
+
return [ordered]@{ op = 'unmerge_cells'; changed = $true; range = [string]$op.range }
|
|
2232
|
+
}
|
|
2233
|
+
'set_autofilter' {
|
|
2234
|
+
$sheet = Excel-Sheet $book $op
|
|
2235
|
+
if ($null -ne $op.enabled -and -not [bool]$op.enabled) {
|
|
2236
|
+
$sheet.AutoFilterMode = $false
|
|
2237
|
+
} else {
|
|
2238
|
+
$sheet.Range([string]$op.range).AutoFilter()
|
|
2239
|
+
}
|
|
2240
|
+
return [ordered]@{ op = 'set_autofilter'; changed = $true; enabled = $(if ($null -ne $op.enabled) { [bool]$op.enabled } else { $true }) }
|
|
2241
|
+
}
|
|
2242
|
+
'set_hyperlink' {
|
|
2243
|
+
$sheet = Excel-Sheet $book $op
|
|
2244
|
+
$target = $sheet.Range([string]$op.cell)
|
|
2245
|
+
try { $target.Hyperlinks.Delete() } catch {}
|
|
2246
|
+
$display = if ($null -ne $op.text) { [string]$op.text } else { [string]$target.Text }
|
|
2247
|
+
$link = $sheet.Hyperlinks.Add($target, [string]$op.address, [string]$op.subAddress, [string]$op.screenTip, $display)
|
|
2248
|
+
return [ordered]@{ op = 'set_hyperlink'; changed = $true; cell = [string]$op.cell; address = [string]$link.Address }
|
|
2249
|
+
}
|
|
2250
|
+
'define_name' {
|
|
2251
|
+
$refersTo = [string]$op.refersTo
|
|
2252
|
+
if (-not $refersTo.StartsWith('=')) { $refersTo = "=$refersTo" }
|
|
2253
|
+
$name = $book.Names.Add([string]$op.name, $refersTo)
|
|
2254
|
+
return [ordered]@{ op = 'define_name'; changed = $true; name = [string]$name.Name; refersTo = [string]$name.RefersTo }
|
|
2255
|
+
}
|
|
2256
|
+
'delete_name' {
|
|
2257
|
+
$book.Names.Item([string]$op.name).Delete()
|
|
2258
|
+
return [ordered]@{ op = 'delete_name'; changed = $true; name = [string]$op.name }
|
|
2259
|
+
}
|
|
2260
|
+
'protect_sheet' {
|
|
2261
|
+
$sheet = Excel-Sheet $book $op
|
|
2262
|
+
$sheet.Protect([string]$op.password)
|
|
2263
|
+
return [ordered]@{ op = 'protect_sheet'; changed = $true; sheet = [string]$sheet.Name }
|
|
2264
|
+
}
|
|
2265
|
+
'unprotect_sheet' {
|
|
2266
|
+
$sheet = Excel-Sheet $book $op
|
|
2267
|
+
$sheet.Unprotect([string]$op.password)
|
|
2268
|
+
return [ordered]@{ op = 'unprotect_sheet'; changed = $true; sheet = [string]$sheet.Name }
|
|
2269
|
+
}
|
|
2270
|
+
'autofit_range' {
|
|
2271
|
+
$sheet = Excel-Sheet $book $op
|
|
2272
|
+
$target = $sheet.Range([string]$op.range)
|
|
2273
|
+
$null = $target.EntireColumn.AutoFit()
|
|
2274
|
+
if ([bool]$op.rows) { $null = $target.EntireRow.AutoFit() }
|
|
2275
|
+
return [ordered]@{ op = 'autofit_range'; changed = $true; range = [string]$op.range }
|
|
2276
|
+
}
|
|
2277
|
+
'set_page_setup' {
|
|
2278
|
+
$sheet = Excel-Sheet $book $op
|
|
2279
|
+
$setup = $sheet.PageSetup
|
|
2280
|
+
if ($op.printArea) { $setup.PrintArea = [string]$op.printArea }
|
|
2281
|
+
if ($op.orientation) { $setup.Orientation = $(if ([string]$op.orientation -eq 'landscape') { 2 } else { 1 }) }
|
|
2282
|
+
if ($null -ne $op.fitToPagesWide -or $null -ne $op.fitToPagesTall) { $setup.Zoom = $false }
|
|
2283
|
+
if ($null -ne $op.fitToPagesWide) { $setup.FitToPagesWide = [int]$op.fitToPagesWide }
|
|
2284
|
+
if ($null -ne $op.fitToPagesTall) { $setup.FitToPagesTall = [int]$op.fitToPagesTall }
|
|
2285
|
+
if ($null -ne $op.centerHorizontally) { $setup.CenterHorizontally = [bool]$op.centerHorizontally }
|
|
2286
|
+
if ($null -ne $op.topMargin) { $setup.TopMargin = [double]$op.topMargin }
|
|
2287
|
+
if ($null -ne $op.bottomMargin) { $setup.BottomMargin = [double]$op.bottomMargin }
|
|
2288
|
+
if ($null -ne $op.leftMargin) { $setup.LeftMargin = [double]$op.leftMargin }
|
|
2289
|
+
if ($null -ne $op.rightMargin) { $setup.RightMargin = [double]$op.rightMargin }
|
|
2290
|
+
return [ordered]@{ op = 'set_page_setup'; changed = $true; sheet = [string]$sheet.Name; printArea = [string]$setup.PrintArea }
|
|
2291
|
+
}
|
|
2292
|
+
'set_sheet_view' {
|
|
2293
|
+
$sheet = Excel-Sheet $book $op
|
|
2294
|
+
$sheet.Activate()
|
|
2295
|
+
$window = $book.Windows.Item(1)
|
|
2296
|
+
if ($null -ne $op.showGridlines) { $window.DisplayGridlines = [bool]$op.showGridlines }
|
|
2297
|
+
if ($null -ne $op.zoom) { $window.Zoom = [Math]::Max(10, [Math]::Min(400, [int]$op.zoom)) }
|
|
2298
|
+
return [ordered]@{ op = 'set_sheet_view'; changed = $true; sheet = [string]$sheet.Name }
|
|
2299
|
+
}
|
|
2300
|
+
'set_sheet_visibility' {
|
|
2301
|
+
$sheet = Excel-Sheet $book $op
|
|
2302
|
+
$visibility = ([string]$op.visibility).ToLowerInvariant()
|
|
2303
|
+
$sheet.Visible = switch ($visibility) {
|
|
2304
|
+
'hidden' { 0 }
|
|
2305
|
+
'very_hidden' { 2 }
|
|
2306
|
+
'veryhidden' { 2 }
|
|
2307
|
+
'visible' { -1 }
|
|
2308
|
+
default { throw "Unknown worksheet visibility: $visibility" }
|
|
2309
|
+
}
|
|
2310
|
+
return [ordered]@{ op = 'set_sheet_visibility'; changed = $true; sheet = [string]$sheet.Name; visibility = $visibility }
|
|
2311
|
+
}
|
|
2312
|
+
default { throw "Unsupported XLSX operation: $($op.op)" }
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function Ppt-Slide($presentation, $op) {
|
|
2317
|
+
return $presentation.Slides.Item([int]$op.slide)
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
function Set-PowerPointChartData($chart, $categoryValues, $seriesValues) {
|
|
2321
|
+
$specs = @($seriesValues)
|
|
2322
|
+
if ($specs.Count -eq 0) { throw 'PowerPoint chart data requires at least one series' }
|
|
2323
|
+
$categories = @($categoryValues)
|
|
2324
|
+
$pointCount = $categories.Count
|
|
2325
|
+
foreach ($spec in $specs) { $pointCount = [Math]::Max($pointCount, @($spec.values).Count) }
|
|
2326
|
+
if ($pointCount -eq 0) { throw 'PowerPoint chart data requires at least one category or value' }
|
|
2327
|
+
if ($categories.Count -eq 0) {
|
|
2328
|
+
$categories = @(1..$pointCount | ForEach-Object { "Item $_" })
|
|
2329
|
+
}
|
|
2330
|
+
$chartData = $null
|
|
2331
|
+
$workbook = $null
|
|
2332
|
+
$worksheet = $null
|
|
2333
|
+
try {
|
|
2334
|
+
$chartData = $chart.ChartData
|
|
2335
|
+
$null = $chartData.Activate()
|
|
2336
|
+
for ($attempt = 0; $attempt -lt 50 -and $null -eq $workbook; $attempt++) {
|
|
2337
|
+
try { $workbook = $chartData.Workbook } catch {}
|
|
2338
|
+
if ($null -eq $workbook) { Start-Sleep -Milliseconds 100 }
|
|
2339
|
+
}
|
|
2340
|
+
if ($null -eq $workbook) { throw 'PowerPoint chart embedded workbook did not activate' }
|
|
2341
|
+
$worksheet = $workbook.Worksheets.Item(1)
|
|
2342
|
+
$null = $worksheet.Cells.Clear()
|
|
2343
|
+
$matrix = New-Object 'object[,]' ($pointCount + 1), ($specs.Count + 1)
|
|
2344
|
+
$matrix[0, 0] = 'Category'
|
|
2345
|
+
for ($seriesIndex = 1; $seriesIndex -le $specs.Count; $seriesIndex++) {
|
|
2346
|
+
$matrix[0, $seriesIndex] = [string]$specs[$seriesIndex - 1].name
|
|
2347
|
+
}
|
|
2348
|
+
for ($pointIndex = 1; $pointIndex -le $pointCount; $pointIndex++) {
|
|
2349
|
+
$category = if ($pointIndex -le $categories.Count) { $categories[$pointIndex - 1] } else { "Item $pointIndex" }
|
|
2350
|
+
$matrix[$pointIndex, 0] = $category
|
|
2351
|
+
for ($seriesIndex = 1; $seriesIndex -le $specs.Count; $seriesIndex++) {
|
|
2352
|
+
$values = @($specs[$seriesIndex - 1].values)
|
|
2353
|
+
if ($pointIndex -le $values.Count) {
|
|
2354
|
+
$matrix[$pointIndex, $seriesIndex] = $values[$pointIndex - 1]
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
$lastColumn = Excel-ColumnLabel ($specs.Count + 1)
|
|
2359
|
+
$source = $worksheet.Range("A1:${lastColumn}$($pointCount + 1)")
|
|
2360
|
+
$null = ($source.Value2 = $matrix)
|
|
2361
|
+
$collection = $chart.SeriesCollection()
|
|
2362
|
+
$placeholderSeriesCount = [int]$collection.Count
|
|
2363
|
+
$categoryArray = New-Object 'object[]' $pointCount
|
|
2364
|
+
for ($pointIndex = 0; $pointIndex -lt $pointCount; $pointIndex++) {
|
|
2365
|
+
$categoryArray[$pointIndex] = if ($pointIndex -lt $categories.Count) { $categories[$pointIndex] } else { "Item $($pointIndex + 1)" }
|
|
2366
|
+
}
|
|
2367
|
+
for ($seriesIndex = 1; $seriesIndex -le $specs.Count; $seriesIndex++) {
|
|
2368
|
+
$series = $collection.NewSeries()
|
|
2369
|
+
$values = @($specs[$seriesIndex - 1].values)
|
|
2370
|
+
$valueArray = New-Object 'object[]' $pointCount
|
|
2371
|
+
for ($pointIndex = 0; $pointIndex -lt $pointCount; $pointIndex++) {
|
|
2372
|
+
if ($pointIndex -lt $values.Count) { $valueArray[$pointIndex] = $values[$pointIndex] }
|
|
2373
|
+
}
|
|
2374
|
+
$null = ($series.Name = [string]$specs[$seriesIndex - 1].name)
|
|
2375
|
+
$null = ($series.XValues = $categoryArray)
|
|
2376
|
+
$null = ($series.Values = $valueArray)
|
|
2377
|
+
}
|
|
2378
|
+
for ($seriesIndex = 0; $seriesIndex -lt $placeholderSeriesCount; $seriesIndex++) { $null = $collection.Item(1).Delete() }
|
|
2379
|
+
$null = $chart.Refresh()
|
|
2380
|
+
} finally {
|
|
2381
|
+
if ($null -ne $workbook) { try { $null = $workbook.Close($true) } catch {} }
|
|
2382
|
+
if ($null -ne $worksheet) { try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($worksheet) } catch {} }
|
|
2383
|
+
if ($null -ne $workbook) { try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) } catch {} }
|
|
2384
|
+
if ($null -ne $chartData) { try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($chartData) } catch {} }
|
|
2385
|
+
}
|
|
2386
|
+
for ($seriesIndex = 1; $seriesIndex -le [Math]::Min($specs.Count, [int]$chart.SeriesCollection().Count); $seriesIndex++) {
|
|
2387
|
+
$spec = $specs[$seriesIndex - 1]
|
|
2388
|
+
if ($spec.color) {
|
|
2389
|
+
try {
|
|
2390
|
+
$chart.SeriesCollection().Item($seriesIndex).Format.Fill.ForeColor.RGB = Color-Value ([string]$spec.color)
|
|
2391
|
+
$chart.SeriesCollection().Item($seriesIndex).Format.Line.ForeColor.RGB = Color-Value ([string]$spec.color)
|
|
2392
|
+
} catch {}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
$null = ($chart.HasLegend = $specs.Count -gt 1)
|
|
2396
|
+
return [ordered]@{ categories = $pointCount; series = [int]$chart.SeriesCollection().Count }
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function Set-PowerPointParagraphs($shape, $paragraphs) {
|
|
2400
|
+
$items = @($paragraphs)
|
|
2401
|
+
$shape.TextFrame.TextRange.Text = (@($items | ForEach-Object { [string]$_.text }) -join "`r")
|
|
2402
|
+
for ($index = 1; $index -le $items.Count; $index++) {
|
|
2403
|
+
$spec = $items[$index - 1]
|
|
2404
|
+
$paragraph = $shape.TextFrame.TextRange.Paragraphs($index, 1)
|
|
2405
|
+
if ($null -ne $spec.level) { $paragraph.IndentLevel = [Math]::Max(1, [Math]::Min(5, [int]$spec.level + 1)) }
|
|
2406
|
+
if ($null -ne $spec.bullet) { $paragraph.ParagraphFormat.Bullet.Visible = $(if ([bool]$spec.bullet) { -1 } else { 0 }) }
|
|
2407
|
+
if ($spec.fontName) { $paragraph.Font.Name = [string]$spec.fontName }
|
|
2408
|
+
if ($spec.fontSize) { $paragraph.Font.Size = [single]$spec.fontSize }
|
|
2409
|
+
if ($null -ne $spec.bold) { $paragraph.Font.Bold = $(if ([bool]$spec.bold) { -1 } else { 0 }) }
|
|
2410
|
+
if ($null -ne $spec.italic) { $paragraph.Font.Italic = $(if ([bool]$spec.italic) { -1 } else { 0 }) }
|
|
2411
|
+
if ($spec.color) { $paragraph.Font.Color.RGB = Color-Value ([string]$spec.color) }
|
|
2412
|
+
}
|
|
2413
|
+
return $items.Count
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function Apply-PowerPointOperation($presentation, $op, [bool]$live = $false) {
|
|
2417
|
+
switch ([string]$op.op) {
|
|
2418
|
+
'fill_template' { return Fill-PowerPointTemplate $presentation $op }
|
|
2419
|
+
'replace_text' {
|
|
2420
|
+
$count = 0
|
|
2421
|
+
foreach ($slide in @($presentation.Slides)) {
|
|
2422
|
+
foreach ($shape in @($slide.Shapes)) {
|
|
2423
|
+
try {
|
|
2424
|
+
if ($shape.HasTextFrame -and $shape.TextFrame.HasText) {
|
|
2425
|
+
$before = [string]$shape.TextFrame.TextRange.Text
|
|
2426
|
+
$after = $before.Replace([string]$op.find, [string]$op.replace)
|
|
2427
|
+
if ($after -ne $before) { $shape.TextFrame.TextRange.Text = $after; $count++ }
|
|
2428
|
+
}
|
|
2429
|
+
} catch {}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return [ordered]@{ op = 'replace_text'; changed = $count -gt 0; shapes = $count }
|
|
2433
|
+
}
|
|
2434
|
+
'set_text' {
|
|
2435
|
+
$slide = Ppt-Slide $presentation $op
|
|
2436
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2437
|
+
$shape.TextFrame.TextRange.Text = [string]$op.text
|
|
2438
|
+
return [ordered]@{ op = 'set_text'; changed = $true }
|
|
2439
|
+
}
|
|
2440
|
+
'add_slide' {
|
|
2441
|
+
$index = if ($op.index) { [int]$op.index } else { $presentation.Slides.Count + 1 }
|
|
2442
|
+
$layout = PowerPoint-Layout $presentation $op.layout
|
|
2443
|
+
$slide = $presentation.Slides.AddSlide($index, $layout.Layout)
|
|
2444
|
+
return [ordered]@{ op = 'add_slide'; changed = $true; slide = [int]$slide.SlideIndex; layout = [int]$layout.Index; layoutName = [string]$layout.Layout.Name }
|
|
2445
|
+
}
|
|
2446
|
+
'delete_slide' {
|
|
2447
|
+
(Ppt-Slide $presentation $op).Delete()
|
|
2448
|
+
return [ordered]@{ op = 'delete_slide'; changed = $true }
|
|
2449
|
+
}
|
|
2450
|
+
'move_slide' {
|
|
2451
|
+
$presentation.Slides.Item([int]$op.slide).MoveTo([int]$op.index)
|
|
2452
|
+
return [ordered]@{ op = 'move_slide'; changed = $true }
|
|
2453
|
+
}
|
|
2454
|
+
'delete_shape' {
|
|
2455
|
+
(Ppt-Slide $presentation $op).Shapes.Item([int]$op.shape).Delete()
|
|
2456
|
+
return [ordered]@{ op = 'delete_shape'; changed = $true }
|
|
2457
|
+
}
|
|
2458
|
+
'add_textbox' {
|
|
2459
|
+
$slide = Ppt-Slide $presentation $op
|
|
2460
|
+
$left = [single](Operation-Property $op 'left' 72)
|
|
2461
|
+
$top = [single](Operation-Property $op 'top' 72)
|
|
2462
|
+
$width = [single](Operation-Property $op 'width' 360)
|
|
2463
|
+
$height = [single](Operation-Property $op 'height' 72)
|
|
2464
|
+
$shape = $slide.Shapes.AddTextbox(1, $left, $top, $width, $height)
|
|
2465
|
+
try { $shape.TextFrame.AutoSize = 0 } catch {}
|
|
2466
|
+
try { $shape.TextFrame2.AutoSize = 0 } catch {}
|
|
2467
|
+
$paragraphs = $op.PSObject.Properties['paragraphs']
|
|
2468
|
+
if ($null -ne $paragraphs -and $null -ne $paragraphs.Value -and @($paragraphs.Value).Count -gt 0) {
|
|
2469
|
+
$null = Set-PowerPointParagraphs $shape $paragraphs.Value
|
|
2470
|
+
} else {
|
|
2471
|
+
$shape.TextFrame.TextRange.Text = [string]$op.text
|
|
2472
|
+
}
|
|
2473
|
+
$shape.Left = $left
|
|
2474
|
+
$shape.Top = $top
|
|
2475
|
+
$shape.Width = $width
|
|
2476
|
+
$shape.Height = $height
|
|
2477
|
+
$fontSize = Operation-Property $op 'fontSize' $null
|
|
2478
|
+
$fontName = Operation-Property $op 'fontName' $null
|
|
2479
|
+
$color = Operation-Property $op 'color' $null
|
|
2480
|
+
if ($null -ne $fontSize) { $shape.TextFrame.TextRange.Font.Size = [single]$fontSize }
|
|
2481
|
+
if ($null -ne $fontName) { $shape.TextFrame.TextRange.Font.Name = [string]$fontName }
|
|
2482
|
+
if ($null -ne $color) { $shape.TextFrame.TextRange.Font.Color.RGB = Color-Value ([string]$color) }
|
|
2483
|
+
$props = $op.properties
|
|
2484
|
+
if ($null -ne $props.bold) { $shape.TextFrame.TextRange.Font.Bold = $(if ([bool]$props.bold) { -1 } else { 0 }) }
|
|
2485
|
+
if ($null -ne $props.italic) { $shape.TextFrame.TextRange.Font.Italic = $(if ([bool]$props.italic) { -1 } else { 0 }) }
|
|
2486
|
+
if ($props.alignment) {
|
|
2487
|
+
$shape.TextFrame.TextRange.ParagraphFormat.Alignment = switch ([string]$props.alignment) {
|
|
2488
|
+
'center' { 2 }
|
|
2489
|
+
'right' { 3 }
|
|
2490
|
+
'justify' { 4 }
|
|
2491
|
+
default { 1 }
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if ($props.verticalAlignment) {
|
|
2495
|
+
$shape.TextFrame.VerticalAnchor = switch ([string]$props.verticalAlignment) {
|
|
2496
|
+
'middle' { 3 }
|
|
2497
|
+
'bottom' { 4 }
|
|
2498
|
+
default { 1 }
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if ($null -ne $props.marginLeft) { $shape.TextFrame.MarginLeft = [single]$props.marginLeft }
|
|
2502
|
+
if ($null -ne $props.marginTop) { $shape.TextFrame.MarginTop = [single]$props.marginTop }
|
|
2503
|
+
if ($null -ne $props.marginRight) { $shape.TextFrame.MarginRight = [single]$props.marginRight }
|
|
2504
|
+
if ($null -ne $props.marginBottom) { $shape.TextFrame.MarginBottom = [single]$props.marginBottom }
|
|
2505
|
+
return [ordered]@{ op = 'add_textbox'; changed = $true; shape = [string]$shape.Name }
|
|
2506
|
+
}
|
|
2507
|
+
'add_shape' {
|
|
2508
|
+
$slide = Ppt-Slide $presentation $op
|
|
2509
|
+
$shapeTypes = @{
|
|
2510
|
+
rectangle = 1
|
|
2511
|
+
rounded_rectangle = 5
|
|
2512
|
+
oval = 9
|
|
2513
|
+
diamond = 4
|
|
2514
|
+
right_arrow = 33
|
|
2515
|
+
chevron = 52
|
|
2516
|
+
}
|
|
2517
|
+
$kind = ([string]$op.shapeType).ToLowerInvariant()
|
|
2518
|
+
$shapeType = if ($shapeTypes.ContainsKey($kind)) { [int]$shapeTypes[$kind] } elseif ($op.shapeType -as [int]) { [int]$op.shapeType } else { 1 }
|
|
2519
|
+
$left = [single](Operation-Property $op 'left' 72)
|
|
2520
|
+
$top = [single](Operation-Property $op 'top' 72)
|
|
2521
|
+
$width = [single](Operation-Property $op 'width' 180)
|
|
2522
|
+
$height = [single](Operation-Property $op 'height' 90)
|
|
2523
|
+
$shape = $slide.Shapes.AddShape($shapeType, $left, $top, $width, $height)
|
|
2524
|
+
$fillColor = Operation-Property $op 'fillColor' $null
|
|
2525
|
+
$lineColor = Operation-Property $op 'lineColor' $null
|
|
2526
|
+
if ($fillColor) { $shape.Fill.Visible = $true; $shape.Fill.ForeColor.RGB = Color-Value ([string]$fillColor) }
|
|
2527
|
+
if ($lineColor) { $shape.Line.Visible = $true; $shape.Line.ForeColor.RGB = Color-Value ([string]$lineColor) }
|
|
2528
|
+
$paragraphs = $op.PSObject.Properties['paragraphs']
|
|
2529
|
+
if ($null -ne $paragraphs -and $null -ne $paragraphs.Value -and @($paragraphs.Value).Count -gt 0) {
|
|
2530
|
+
$null = Set-PowerPointParagraphs $shape $paragraphs.Value
|
|
2531
|
+
} elseif ($null -ne $op.text) {
|
|
2532
|
+
$shape.TextFrame.TextRange.Text = [string]$op.text
|
|
2533
|
+
}
|
|
2534
|
+
return [ordered]@{ op = 'add_shape'; changed = $true; shape = [string]$shape.Name; shapeType = $shapeType }
|
|
2535
|
+
}
|
|
2536
|
+
'add_table' {
|
|
2537
|
+
$slide = Ppt-Slide $presentation $op
|
|
2538
|
+
$values = @($op.values)
|
|
2539
|
+
$rows = if ($op.rows) { [int]$op.rows } else { [Math]::Max(1, $values.Count) }
|
|
2540
|
+
$columns = if ($op.columns) { [int]$op.columns } elseif ($values.Count -gt 0) { [Math]::Max(1, @($values[0]).Count) } else { 1 }
|
|
2541
|
+
$left = [single](Operation-Property $op 'left' 72)
|
|
2542
|
+
$top = [single](Operation-Property $op 'top' 72)
|
|
2543
|
+
$width = [single](Operation-Property $op 'width' 480)
|
|
2544
|
+
$height = [single](Operation-Property $op 'height' 180)
|
|
2545
|
+
$shape = $slide.Shapes.AddTable($rows, $columns, $left, $top, $width, $height)
|
|
2546
|
+
if ($op.properties.headerRowHeight) { $shape.Table.Rows.Item(1).Height = [single]$op.properties.headerRowHeight }
|
|
2547
|
+
if ($op.properties.bodyRowHeight) {
|
|
2548
|
+
for ($row = 2; $row -le $rows; $row++) { $shape.Table.Rows.Item($row).Height = [single]$op.properties.bodyRowHeight }
|
|
2549
|
+
}
|
|
2550
|
+
for ($row = 1; $row -le $rows; $row++) {
|
|
2551
|
+
for ($column = 1; $column -le $columns; $column++) {
|
|
2552
|
+
$value = if ($row -le $values.Count -and $column -le @($values[$row - 1]).Count) { $values[$row - 1][$column - 1] } else { '' }
|
|
2553
|
+
$cell = $shape.Table.Cell($row, $column).Shape
|
|
2554
|
+
$cell.TextFrame.TextRange.Text = [string]$value
|
|
2555
|
+
$props = $op.properties
|
|
2556
|
+
if ($props.fontName) { $cell.TextFrame.TextRange.Font.Name = [string]$props.fontName }
|
|
2557
|
+
if ($props.fontSize) { $cell.TextFrame.TextRange.Font.Size = [single]$props.fontSize }
|
|
2558
|
+
if ($props.color) { $cell.TextFrame.TextRange.Font.Color.RGB = Color-Value ([string]$props.color) }
|
|
2559
|
+
if ($row -eq 1) {
|
|
2560
|
+
if ($props.headerFillColor) { $cell.Fill.ForeColor.RGB = Color-Value ([string]$props.headerFillColor) }
|
|
2561
|
+
if ($props.headerColor) { $cell.TextFrame.TextRange.Font.Color.RGB = Color-Value ([string]$props.headerColor) }
|
|
2562
|
+
$cell.TextFrame.TextRange.Font.Bold = -1
|
|
2563
|
+
} elseif ($props.bodyFillColor) {
|
|
2564
|
+
$cell.Fill.ForeColor.RGB = Color-Value ([string]$props.bodyFillColor)
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return [ordered]@{ op = 'add_table'; changed = $true; shape = [string]$shape.Name; rows = $rows; columns = $columns }
|
|
2569
|
+
}
|
|
2570
|
+
'set_table_data' {
|
|
2571
|
+
$slide = Ppt-Slide $presentation $op
|
|
2572
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2573
|
+
if (-not $shape.HasTable) { throw "PowerPoint shape $($op.shape) is not a table" }
|
|
2574
|
+
$values = @($op.values)
|
|
2575
|
+
$availableRows = [int]$shape.Table.Rows.Count
|
|
2576
|
+
$availableColumns = [int]$shape.Table.Columns.Count
|
|
2577
|
+
$requestedColumns = 0
|
|
2578
|
+
foreach ($rowValues in $values) {
|
|
2579
|
+
$requestedColumns = [Math]::Max($requestedColumns, @($rowValues).Count)
|
|
2580
|
+
}
|
|
2581
|
+
if ($values.Count -gt $availableRows -or $requestedColumns -gt $availableColumns) {
|
|
2582
|
+
throw "PowerPoint table shape $($op.shape) is ${availableRows}x${availableColumns}, but received $($values.Count)x${requestedColumns}"
|
|
2583
|
+
}
|
|
2584
|
+
for ($row = 1; $row -le $availableRows; $row++) {
|
|
2585
|
+
for ($column = 1; $column -le $availableColumns; $column++) {
|
|
2586
|
+
$value = if ($row -le $values.Count -and $column -le @($values[$row - 1]).Count) { $values[$row - 1][$column - 1] } else { '' }
|
|
2587
|
+
$shape.Table.Cell($row, $column).Shape.TextFrame.TextRange.Text = [string]$value
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
return [ordered]@{ op = 'set_table_data'; changed = $true; shape = [int]$op.shape; rows = $availableRows; columns = $availableColumns }
|
|
2591
|
+
}
|
|
2592
|
+
'set_hyperlink' {
|
|
2593
|
+
$slide = Ppt-Slide $presentation $op
|
|
2594
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2595
|
+
$link = $shape.ActionSettings(1).Hyperlink
|
|
2596
|
+
$link.Address = [string]$op.address
|
|
2597
|
+
$link.SubAddress = [string]$op.subAddress
|
|
2598
|
+
return [ordered]@{ op = 'set_hyperlink'; changed = $true; shape = [int]$op.shape; address = [string]$link.Address }
|
|
2599
|
+
}
|
|
2600
|
+
'z_order' {
|
|
2601
|
+
$slide = Ppt-Slide $presentation $op
|
|
2602
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2603
|
+
$commands = @{ front = 0; back = 1; forward = 2; backward = 3 }
|
|
2604
|
+
$command = ([string]$op.command).ToLowerInvariant()
|
|
2605
|
+
if (-not $commands.ContainsKey($command)) { throw "Unsupported z-order command: $command" }
|
|
2606
|
+
$null = $shape.ZOrder([int]$commands[$command])
|
|
2607
|
+
return [ordered]@{ op = 'z_order'; changed = $true; shape = [int]$op.shape; command = $command }
|
|
2608
|
+
}
|
|
2609
|
+
'align_shapes' {
|
|
2610
|
+
$slide = Ppt-Slide $presentation $op
|
|
2611
|
+
$commands = @{ left = 0; center = 1; right = 2; top = 3; middle = 4; bottom = 5 }
|
|
2612
|
+
$command = ([string]$op.align).ToLowerInvariant()
|
|
2613
|
+
if (-not $commands.ContainsKey($command)) { throw "Unsupported shape alignment: $command" }
|
|
2614
|
+
$indices = [object[]]@($op.shapes | ForEach-Object { [int]$_ })
|
|
2615
|
+
$null = $slide.Shapes.Range($indices).Align([int]$commands[$command], [bool]$op.relativeToSlide)
|
|
2616
|
+
return [ordered]@{ op = 'align_shapes'; changed = $true; shapes = @($op.shapes); align = $command }
|
|
2617
|
+
}
|
|
2618
|
+
'distribute_shapes' {
|
|
2619
|
+
$slide = Ppt-Slide $presentation $op
|
|
2620
|
+
$commands = @{ horizontal = 0; vertical = 1 }
|
|
2621
|
+
$command = ([string]$op.direction).ToLowerInvariant()
|
|
2622
|
+
if (-not $commands.ContainsKey($command)) { throw "Unsupported shape distribution: $command" }
|
|
2623
|
+
$indices = [object[]]@($op.shapes | ForEach-Object { [int]$_ })
|
|
2624
|
+
$null = $slide.Shapes.Range($indices).Distribute([int]$commands[$command], [bool]$op.relativeToSlide)
|
|
2625
|
+
return [ordered]@{ op = 'distribute_shapes'; changed = $true; shapes = @($op.shapes); direction = $command }
|
|
2626
|
+
}
|
|
2627
|
+
'group_shapes' {
|
|
2628
|
+
$slide = Ppt-Slide $presentation $op
|
|
2629
|
+
$indices = [object[]]@($op.shapes | ForEach-Object { [int]$_ })
|
|
2630
|
+
if ($indices.Count -lt 2) { throw 'group_shapes requires at least two shape indexes' }
|
|
2631
|
+
$shape = $slide.Shapes.Range($indices).Group()
|
|
2632
|
+
return [ordered]@{ op = 'group_shapes'; changed = $true; shape = [int]$shape.ZOrderPosition; name = [string]$shape.Name }
|
|
2633
|
+
}
|
|
2634
|
+
'ungroup_shape' {
|
|
2635
|
+
$slide = Ppt-Slide $presentation $op
|
|
2636
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2637
|
+
$items = $shape.Ungroup()
|
|
2638
|
+
return [ordered]@{ op = 'ungroup_shape'; changed = $true; count = [int]$items.Count }
|
|
2639
|
+
}
|
|
2640
|
+
'set_notes' {
|
|
2641
|
+
$slide = Ppt-Slide $presentation $op
|
|
2642
|
+
$slide.NotesPage.Shapes.Placeholders.Item(2).TextFrame.TextRange.Text = [string]$op.text
|
|
2643
|
+
return [ordered]@{ op = 'set_notes'; changed = $true }
|
|
2644
|
+
}
|
|
2645
|
+
'add_comment' {
|
|
2646
|
+
$slide = Ppt-Slide $presentation $op
|
|
2647
|
+
$left = if ($null -ne $op.left) { [single]$op.left } else { [single]20 }
|
|
2648
|
+
$top = if ($null -ne $op.top) { [single]$op.top } else { [single]20 }
|
|
2649
|
+
$author = if ($op.author) { [string]$op.author } else { 'Mixdog' }
|
|
2650
|
+
$initials = if ($op.initials) { [string]$op.initials } else { 'MD' }
|
|
2651
|
+
$comment = $slide.Comments.Add($left, $top, $author, $initials, [string]$op.text)
|
|
2652
|
+
return [ordered]@{ op = 'add_comment'; changed = $true; slide = [int]$slide.SlideIndex; comment = [int]$comment.Index }
|
|
2653
|
+
}
|
|
2654
|
+
'delete_comment' {
|
|
2655
|
+
$slide = Ppt-Slide $presentation $op
|
|
2656
|
+
$index = [int]$op.comment
|
|
2657
|
+
if ($index -lt 1 -or $index -gt $slide.Comments.Count) { throw "PowerPoint comment index out of range: $index" }
|
|
2658
|
+
$slide.Comments.Item($index).Delete()
|
|
2659
|
+
return [ordered]@{ op = 'delete_comment'; changed = $true; slide = [int]$slide.SlideIndex; comment = $index }
|
|
2660
|
+
}
|
|
2661
|
+
'add_provenance' {
|
|
2662
|
+
$slide = Ppt-Slide $presentation $op
|
|
2663
|
+
$text = Provenance-Text $op
|
|
2664
|
+
$range = $slide.NotesPage.Shapes.Placeholders.Item(2).TextFrame.TextRange
|
|
2665
|
+
$existing = [string]$range.Text
|
|
2666
|
+
if ($existing -notmatch [regex]::Escape($text)) {
|
|
2667
|
+
$range.Text = $(if ([string]::IsNullOrWhiteSpace($existing)) { $text } else { "$($existing.TrimEnd())`r$text" })
|
|
2668
|
+
}
|
|
2669
|
+
$target = if ($op.shape) { "/slide[$([int]$slide.SlideIndex)]/shape[$([int]$op.shape)]" } else { "/slide[$([int]$slide.SlideIndex)]" }
|
|
2670
|
+
return [ordered]@{ op = 'add_provenance'; changed = $true; target = $target; citation = $text }
|
|
2671
|
+
}
|
|
2672
|
+
'set_footer' {
|
|
2673
|
+
$slide = Ppt-Slide $presentation $op
|
|
2674
|
+
$slide.HeadersFooters.Footer.Visible = -1
|
|
2675
|
+
$slide.HeadersFooters.Footer.Text = [string]$op.text
|
|
2676
|
+
return [ordered]@{ op = 'set_footer'; changed = $true }
|
|
2677
|
+
}
|
|
2678
|
+
'set_slide_number' {
|
|
2679
|
+
$slide = Ppt-Slide $presentation $op
|
|
2680
|
+
$slide.HeadersFooters.SlideNumber.Visible = $(if ([bool]$op.visible) { -1 } else { 0 })
|
|
2681
|
+
return [ordered]@{ op = 'set_slide_number'; changed = $true; visible = [bool]$op.visible }
|
|
2682
|
+
}
|
|
2683
|
+
'add_image' {
|
|
2684
|
+
$slide = Ppt-Slide $presentation $op
|
|
2685
|
+
$left = if ($op.left) { [single]$op.left } else { [single]72 }
|
|
2686
|
+
$top = if ($op.top) { [single]$op.top } else { [single]72 }
|
|
2687
|
+
$width = if ($op.width) { [single]$op.width } else { [single]320 }
|
|
2688
|
+
$height = if ($op.height) { [single]$op.height } else { [single]240 }
|
|
2689
|
+
$null = $slide.Shapes.AddPicture([string]$op.path, $false, $true, $left, $top, $width, $height)
|
|
2690
|
+
return [ordered]@{ op = 'add_image'; changed = $true }
|
|
2691
|
+
}
|
|
2692
|
+
'replace_image' {
|
|
2693
|
+
$slide = Ppt-Slide $presentation $op
|
|
2694
|
+
$old = $slide.Shapes.Item([int]$op.shape)
|
|
2695
|
+
$left = [single]$old.Left; $top = [single]$old.Top; $width = [single]$old.Width; $height = [single]$old.Height
|
|
2696
|
+
$old.Delete()
|
|
2697
|
+
$shape = $slide.Shapes.AddPicture([string]$op.path, $false, $true, $left, $top, $width, $height)
|
|
2698
|
+
return [ordered]@{ op = 'replace_image'; changed = $true; shape = [string]$shape.Name }
|
|
2699
|
+
}
|
|
2700
|
+
'crop_image' {
|
|
2701
|
+
$slide = Ppt-Slide $presentation $op
|
|
2702
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2703
|
+
if ($null -ne $op.left) { $shape.PictureFormat.CropLeft = [single]$op.left }
|
|
2704
|
+
if ($null -ne $op.right) { $shape.PictureFormat.CropRight = [single]$op.right }
|
|
2705
|
+
if ($null -ne $op.top) { $shape.PictureFormat.CropTop = [single]$op.top }
|
|
2706
|
+
if ($null -ne $op.bottom) { $shape.PictureFormat.CropBottom = [single]$op.bottom }
|
|
2707
|
+
return [ordered]@{ op = 'crop_image'; changed = $true; shape = [int]$op.shape }
|
|
2708
|
+
}
|
|
2709
|
+
'add_media' {
|
|
2710
|
+
$slide = Ppt-Slide $presentation $op
|
|
2711
|
+
$left = if ($null -ne $op.left) { [single]$op.left } else { [single]40 }
|
|
2712
|
+
$top = if ($null -ne $op.top) { [single]$op.top } else { [single]40 }
|
|
2713
|
+
$width = if ($op.width) { [single]$op.width } else { [single]320 }
|
|
2714
|
+
$height = if ($op.height) { [single]$op.height } else { [single]180 }
|
|
2715
|
+
$link = [bool]$op.link
|
|
2716
|
+
$embed = if ($null -ne $op.embed) { [bool]$op.embed } else { -not $link }
|
|
2717
|
+
$shape = $slide.Shapes.AddMediaObject2([string]$op.path, $(if ($link) { -1 } else { 0 }), $(if ($embed) { -1 } else { 0 }), $left, $top, $width, $height)
|
|
2718
|
+
return [ordered]@{ op = 'add_media'; changed = $true; shape = [int]$shape.ZOrderPosition; media = [string]$op.path; embedded = $embed }
|
|
2719
|
+
}
|
|
2720
|
+
'apply_theme' {
|
|
2721
|
+
if (-not $op.path) { throw 'apply_theme requires path' }
|
|
2722
|
+
$presentation.ApplyTheme([string]$op.path)
|
|
2723
|
+
return [ordered]@{ op = 'apply_theme'; changed = $true; path = [string]$op.path }
|
|
2724
|
+
}
|
|
2725
|
+
'set_transition' {
|
|
2726
|
+
$slide = Ppt-Slide $presentation $op
|
|
2727
|
+
$effects = @{ none = 0; fade = 3849; push = 3850; wipe = 3844; split = 3586; reveal = 3847; random = 513 }
|
|
2728
|
+
$kind = ([string]$op.effect).ToLowerInvariant()
|
|
2729
|
+
if ($kind) { $slide.SlideShowTransition.EntryEffect = $(if ($effects.ContainsKey($kind)) { [int]$effects[$kind] } elseif ($op.effect -as [int]) { [int]$op.effect } else { throw "Unknown transition effect: $kind" }) }
|
|
2730
|
+
if ($null -ne $op.advanceOnTime) { $slide.SlideShowTransition.AdvanceOnTime = [bool]$op.advanceOnTime }
|
|
2731
|
+
if ($null -ne $op.advanceTime) { $slide.SlideShowTransition.AdvanceTime = [single]$op.advanceTime }
|
|
2732
|
+
if ($null -ne $op.duration) {
|
|
2733
|
+
$slide.SlideShowTransition.Speed = if ([single]$op.duration -le 0.5) { 1 } elseif ([single]$op.duration -le 1.5) { 2 } else { 3 }
|
|
2734
|
+
}
|
|
2735
|
+
return [ordered]@{ op = 'set_transition'; changed = $true; slide = [int]$slide.SlideIndex; effect = [int]$slide.SlideShowTransition.EntryEffect }
|
|
2736
|
+
}
|
|
2737
|
+
'add_animation' {
|
|
2738
|
+
$slide = Ppt-Slide $presentation $op
|
|
2739
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2740
|
+
$effects = @{ appear = 1; fly = 2; fade = 10; wipe = 22; zoom = 23; float = 30 }
|
|
2741
|
+
$triggers = @{ onclick = 1; withprevious = 2; afterprevious = 3 }
|
|
2742
|
+
$effectKey = ([string]$op.effect).Replace(' ', '').ToLowerInvariant()
|
|
2743
|
+
$triggerKey = ([string]$op.trigger).Replace(' ', '').ToLowerInvariant()
|
|
2744
|
+
$effectType = if ($effects.ContainsKey($effectKey)) { [int]$effects[$effectKey] } elseif ($op.effect -as [int]) { [int]$op.effect } else { 10 }
|
|
2745
|
+
$triggerType = if ($triggers.ContainsKey($triggerKey)) { [int]$triggers[$triggerKey] } elseif ($op.trigger -as [int]) { [int]$op.trigger } else { 1 }
|
|
2746
|
+
$effect = $slide.TimeLine.MainSequence.AddEffect($shape, $effectType, 0, $triggerType)
|
|
2747
|
+
if ($null -ne $op.duration) { $effect.Timing.Duration = [single]$op.duration }
|
|
2748
|
+
if ($null -ne $op.delay) { $effect.Timing.TriggerDelayTime = [single]$op.delay }
|
|
2749
|
+
return [ordered]@{ op = 'add_animation'; changed = $true; slide = [int]$slide.SlideIndex; shape = [int]$op.shape; effect = $effectType }
|
|
2750
|
+
}
|
|
2751
|
+
'set_shape' {
|
|
2752
|
+
$slide = Ppt-Slide $presentation $op
|
|
2753
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2754
|
+
$props = $op.properties
|
|
2755
|
+
if ($null -ne $props.left) { $shape.Left = [single]$props.left }
|
|
2756
|
+
if ($null -ne $props.top) { $shape.Top = [single]$props.top }
|
|
2757
|
+
if ($props.width) { $shape.Width = [single]$props.width }
|
|
2758
|
+
if ($props.height) { $shape.Height = [single]$props.height }
|
|
2759
|
+
if ($null -ne $props.rotation) { $shape.Rotation = [single]$props.rotation }
|
|
2760
|
+
if ($props.fillColor) { $shape.Fill.Visible = $true; $shape.Fill.ForeColor.RGB = Color-Value ([string]$props.fillColor) }
|
|
2761
|
+
if ($null -ne $props.fillTransparency) { $shape.Fill.Transparency = [single]$props.fillTransparency }
|
|
2762
|
+
if ($props.lineColor) { $shape.Line.Visible = $true; $shape.Line.ForeColor.RGB = Color-Value ([string]$props.lineColor) }
|
|
2763
|
+
if ($null -ne $props.lineTransparency) { $shape.Line.Transparency = [single]$props.lineTransparency }
|
|
2764
|
+
if ($props.shadow) {
|
|
2765
|
+
$shape.Shadow.Visible = -1
|
|
2766
|
+
if ($props.shadow.color) { $shape.Shadow.ForeColor.RGB = Color-Value ([string]$props.shadow.color) }
|
|
2767
|
+
if ($null -ne $props.shadow.transparency) { $shape.Shadow.Transparency = [single]$props.shadow.transparency }
|
|
2768
|
+
if ($null -ne $props.shadow.blur) { $shape.Shadow.Blur = [single]$props.shadow.blur }
|
|
2769
|
+
if ($null -ne $props.shadow.offsetX) { $shape.Shadow.OffsetX = [single]$props.shadow.offsetX }
|
|
2770
|
+
if ($null -ne $props.shadow.offsetY) { $shape.Shadow.OffsetY = [single]$props.shadow.offsetY }
|
|
2771
|
+
}
|
|
2772
|
+
try {
|
|
2773
|
+
if ($null -ne $props.marginLeft) { $shape.TextFrame.MarginLeft = [single]$props.marginLeft }
|
|
2774
|
+
if ($null -ne $props.marginTop) { $shape.TextFrame.MarginTop = [single]$props.marginTop }
|
|
2775
|
+
if ($null -ne $props.marginRight) { $shape.TextFrame.MarginRight = [single]$props.marginRight }
|
|
2776
|
+
if ($null -ne $props.marginBottom) { $shape.TextFrame.MarginBottom = [single]$props.marginBottom }
|
|
2777
|
+
if ($null -ne $props.paragraphSpacing) { $shape.TextFrame.TextRange.ParagraphFormat.SpaceAfter = [single]$props.paragraphSpacing }
|
|
2778
|
+
$font = $shape.TextFrame.TextRange.Font
|
|
2779
|
+
if ($props.fontName) { $font.Name = [string]$props.fontName }
|
|
2780
|
+
if ($props.fontSize) { $font.Size = [single]$props.fontSize }
|
|
2781
|
+
if ($null -ne $props.bold) { $font.Bold = if ($props.bold) { -1 } else { 0 } }
|
|
2782
|
+
if ($null -ne $props.italic) { $font.Italic = if ($props.italic) { -1 } else { 0 } }
|
|
2783
|
+
if ($props.color) { $font.Color.RGB = Color-Value ([string]$props.color) }
|
|
2784
|
+
} catch {}
|
|
2785
|
+
return [ordered]@{ op = 'set_shape'; changed = $true; shape = [string]$shape.Name }
|
|
2786
|
+
}
|
|
2787
|
+
'duplicate_slide' {
|
|
2788
|
+
$slides = $presentation.Slides.Item([int]$op.slide).Duplicate()
|
|
2789
|
+
$slide = $slides.Item(1)
|
|
2790
|
+
if ($op.index) { $slide.MoveTo([int]$op.index) }
|
|
2791
|
+
return [ordered]@{ op = 'duplicate_slide'; changed = $true; slide = [int]$slide.SlideIndex }
|
|
2792
|
+
}
|
|
2793
|
+
'import_slides' {
|
|
2794
|
+
if ([int]$presentation.Slides.Count -eq 0) {
|
|
2795
|
+
throw 'import_slides cannot target a zero-slide presentation; import as the first batch of a new background deck'
|
|
2796
|
+
}
|
|
2797
|
+
$after = if ($null -ne $op.after) { [int]$op.after } else { [int]$presentation.Slides.Count }
|
|
2798
|
+
$inserted = 0
|
|
2799
|
+
$sourcePath = [string]$op.path
|
|
2800
|
+
$openPresentations = $presentation.Application.Presentations
|
|
2801
|
+
for ($candidateIndex = 1; $candidateIndex -le [int]$openPresentations.Count; $candidateIndex++) {
|
|
2802
|
+
$candidate = $openPresentations.Item($candidateIndex)
|
|
2803
|
+
try {
|
|
2804
|
+
if (
|
|
2805
|
+
-not [object]::ReferenceEquals($candidate, $presentation) -and
|
|
2806
|
+
[string]::Equals(
|
|
2807
|
+
[System.IO.Path]::GetFullPath([string]$candidate.FullName),
|
|
2808
|
+
[System.IO.Path]::GetFullPath($sourcePath),
|
|
2809
|
+
[System.StringComparison]::OrdinalIgnoreCase
|
|
2810
|
+
)
|
|
2811
|
+
) {
|
|
2812
|
+
throw 'import_slides requires the saved source deck to be closed before import'
|
|
2813
|
+
}
|
|
2814
|
+
} catch {
|
|
2815
|
+
if ($_.Exception.Message -match '^import_slides requires') { throw }
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if ($op.slides) {
|
|
2819
|
+
foreach ($sourceSlide in @($op.slides)) {
|
|
2820
|
+
$count = [int]$presentation.Slides.InsertFromFile($sourcePath, $after + $inserted, [int]$sourceSlide, [int]$sourceSlide)
|
|
2821
|
+
$inserted += $count
|
|
2822
|
+
}
|
|
2823
|
+
} else {
|
|
2824
|
+
$inserted = [int]$presentation.Slides.InsertFromFile($sourcePath, $after)
|
|
2825
|
+
}
|
|
2826
|
+
return [ordered]@{ op = 'import_slides'; changed = $inserted -gt 0; count = $inserted; after = $after; source = $sourcePath }
|
|
2827
|
+
}
|
|
2828
|
+
'keep_slides' {
|
|
2829
|
+
$keep = @{}
|
|
2830
|
+
foreach ($slideNumber in @($op.slides)) { $keep[[int]$slideNumber] = $true }
|
|
2831
|
+
$removed = 0
|
|
2832
|
+
for ($index = $presentation.Slides.Count; $index -ge 1; $index--) {
|
|
2833
|
+
if (-not $keep.ContainsKey($index)) { $presentation.Slides.Item($index).Delete(); $removed++ }
|
|
2834
|
+
}
|
|
2835
|
+
return [ordered]@{ op = 'keep_slides'; changed = $removed -gt 0; removed = $removed; remaining = [int]$presentation.Slides.Count }
|
|
2836
|
+
}
|
|
2837
|
+
'set_slide_background' {
|
|
2838
|
+
$slide = Ppt-Slide $presentation $op
|
|
2839
|
+
$slide.FollowMasterBackground = $false
|
|
2840
|
+
$slide.Background.Fill.Visible = $true
|
|
2841
|
+
$slide.Background.Fill.ForeColor.RGB = Color-Value ([string]$op.color)
|
|
2842
|
+
return [ordered]@{ op = 'set_slide_background'; changed = $true }
|
|
2843
|
+
}
|
|
2844
|
+
'set_layout' {
|
|
2845
|
+
$slide = Ppt-Slide $presentation $op
|
|
2846
|
+
$layout = PowerPoint-Layout $presentation $op.layout
|
|
2847
|
+
$slide.CustomLayout = $layout.Layout
|
|
2848
|
+
return [ordered]@{ op = 'set_layout'; changed = $true; layout = [int]$layout.Index; layoutName = [string]$layout.Layout.Name }
|
|
2849
|
+
}
|
|
2850
|
+
'add_chart' {
|
|
2851
|
+
$slide = Ppt-Slide $presentation $op
|
|
2852
|
+
if ($presentation.Windows.Count -eq 0) {
|
|
2853
|
+
$window = $presentation.NewWindow()
|
|
2854
|
+
try { $window.WindowState = 2 } catch {}
|
|
2855
|
+
}
|
|
2856
|
+
$chartTypes = @{ column = 51; bar = 57; line = 4; pie = 5; area = 1; scatter = -4169 }
|
|
2857
|
+
$kind = ([string]$op.chartType).ToLowerInvariant()
|
|
2858
|
+
$chartType = if ($kind -and $chartTypes.ContainsKey($kind)) { [int]$chartTypes[$kind] } elseif ($op.chartType -as [int]) { [int]$op.chartType } else { 51 }
|
|
2859
|
+
$left = if ($null -ne $op.left) { [single]$op.left } else { [single]72 }
|
|
2860
|
+
$top = if ($null -ne $op.top) { [single]$op.top } else { [single]72 }
|
|
2861
|
+
$width = if ($op.width) { [single]$op.width } else { [single]480 }
|
|
2862
|
+
$height = if ($op.height) { [single]$op.height } else { [single]280 }
|
|
2863
|
+
$shape = $slide.Shapes.AddChart2(-1, $chartType, $left, $top, $width, $height)
|
|
2864
|
+
$chartResult = $null
|
|
2865
|
+
$seriesProperty = $op.PSObject.Properties['series']
|
|
2866
|
+
if ($null -ne $seriesProperty -and $null -ne $seriesProperty.Value) {
|
|
2867
|
+
$chartResult = Set-PowerPointChartData $shape.Chart $op.categories $seriesProperty.Value
|
|
2868
|
+
}
|
|
2869
|
+
if ($op.title) { $shape.Chart.HasTitle = $true; $shape.Chart.ChartTitle.Text = [string]$op.title }
|
|
2870
|
+
return [ordered]@{
|
|
2871
|
+
op = 'add_chart'
|
|
2872
|
+
changed = $true
|
|
2873
|
+
shape = [string]$shape.Name
|
|
2874
|
+
categories = $(if ($null -ne $chartResult) { [int]$chartResult.categories } else { 0 })
|
|
2875
|
+
series = $(if ($null -ne $chartResult) { [int]$chartResult.series } else { [int]$shape.Chart.SeriesCollection().Count })
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
'set_chart_data' {
|
|
2879
|
+
$slide = Ppt-Slide $presentation $op
|
|
2880
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2881
|
+
if (-not $shape.HasChart) { throw "Shape $($op.shape) is not a chart" }
|
|
2882
|
+
$chart = $shape.Chart
|
|
2883
|
+
$chartResult = Set-PowerPointChartData $chart $op.categories $op.series
|
|
2884
|
+
if ($op.title) { $chart.HasTitle = $true; $chart.ChartTitle.Text = [string]$op.title }
|
|
2885
|
+
return [ordered]@{ op = 'set_chart_data'; changed = $true; shape = [int]$op.shape; categories = [int]$chartResult.categories; series = [int]$chartResult.series }
|
|
2886
|
+
}
|
|
2887
|
+
'set_chart_series' {
|
|
2888
|
+
$slide = Ppt-Slide $presentation $op
|
|
2889
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2890
|
+
if (-not $shape.HasChart) { throw "Shape $($op.shape) is not a chart" }
|
|
2891
|
+
$series = $shape.Chart.SeriesCollection().Item([int]$op.series)
|
|
2892
|
+
if ($null -ne $op.name) { $series.Name = [string]$op.name }
|
|
2893
|
+
if ($null -ne $op.categories) { $series.XValues = [object[]]@($op.categories) }
|
|
2894
|
+
if ($null -ne $op.values) { $series.Values = [object[]]@($op.values) }
|
|
2895
|
+
if ($null -ne $op.secondaryAxis) { $series.AxisGroup = $(if ([bool]$op.secondaryAxis) { 2 } else { 1 }) }
|
|
2896
|
+
if ($null -ne $op.chartType) {
|
|
2897
|
+
$chartTypes = @{ column = 51; bar = 57; line = 4; pie = 5; area = 1; scatter = -4169 }
|
|
2898
|
+
$kind = ([string]$op.chartType).ToLowerInvariant()
|
|
2899
|
+
$series.ChartType = $(if ($chartTypes.ContainsKey($kind)) { [int]$chartTypes[$kind] } elseif ($op.chartType -as [int]) { [int]$op.chartType } else { throw "Unknown chart type: $kind" })
|
|
2900
|
+
}
|
|
2901
|
+
return [ordered]@{ op = 'set_chart_series'; changed = $true; shape = [int]$op.shape; series = [int]$op.series }
|
|
2902
|
+
}
|
|
2903
|
+
'set_chart_axis' {
|
|
2904
|
+
$slide = Ppt-Slide $presentation $op
|
|
2905
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2906
|
+
if (-not $shape.HasChart) { throw "Shape $($op.shape) is not a chart" }
|
|
2907
|
+
$axisType = if ([string]$op.axis -eq 'category') { 1 } else { 2 }
|
|
2908
|
+
$axis = $shape.Chart.Axes($axisType, $(if ([bool]$op.secondaryAxis) { 2 } else { 1 }))
|
|
2909
|
+
if ($null -ne $op.title) { $axis.HasTitle = $true; $axis.AxisTitle.Text = [string]$op.title }
|
|
2910
|
+
if ($null -ne $op.minimum) { $axis.MinimumScale = [double]$op.minimum }
|
|
2911
|
+
if ($null -ne $op.maximum) { $axis.MaximumScale = [double]$op.maximum }
|
|
2912
|
+
if ($null -ne $op.majorUnit) { $axis.MajorUnit = [double]$op.majorUnit }
|
|
2913
|
+
if ($op.numberFormat) { $axis.TickLabels.NumberFormat = [string]$op.numberFormat }
|
|
2914
|
+
return [ordered]@{ op = 'set_chart_axis'; changed = $true; shape = [int]$op.shape; axis = [string]$op.axis }
|
|
2915
|
+
}
|
|
2916
|
+
'set_chart_trendline' {
|
|
2917
|
+
$slide = Ppt-Slide $presentation $op
|
|
2918
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2919
|
+
if (-not $shape.HasChart) { throw "Shape $($op.shape) is not a chart" }
|
|
2920
|
+
$seriesIndex = if ($op.series) { [int]$op.series } else { 1 }
|
|
2921
|
+
$types = @{ exponential = 5; linear = -4132; logarithmic = -4133; movingaverage = 6; polynomial = 3; power = 4 }
|
|
2922
|
+
$kind = ([string]$op.type).Replace(' ', '').ToLowerInvariant()
|
|
2923
|
+
$type = if ($types.ContainsKey($kind)) { [int]$types[$kind] } elseif ($op.type -as [int]) { [int]$op.type } else { -4132 }
|
|
2924
|
+
$trendline = $shape.Chart.SeriesCollection().Item($seriesIndex).Trendlines().Add($type)
|
|
2925
|
+
if ($null -ne $op.displayEquation) { $trendline.DisplayEquation = [bool]$op.displayEquation }
|
|
2926
|
+
if ($null -ne $op.displayRSquared) { $trendline.DisplayRSquared = [bool]$op.displayRSquared }
|
|
2927
|
+
return [ordered]@{ op = 'set_chart_trendline'; changed = $true; shape = [int]$op.shape; series = $seriesIndex; type = $type }
|
|
2928
|
+
}
|
|
2929
|
+
'set_chart_error_bars' {
|
|
2930
|
+
$slide = Ppt-Slide $presentation $op
|
|
2931
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2932
|
+
if (-not $shape.HasChart) { throw "Shape $($op.shape) is not a chart" }
|
|
2933
|
+
$seriesIndex = if ($op.series) { [int]$op.series } else { 1 }
|
|
2934
|
+
$series = $shape.Chart.SeriesCollection().Item($seriesIndex)
|
|
2935
|
+
$direction = if ([string]$op.direction -eq 'x') { -4168 } else { 1 }
|
|
2936
|
+
$series.ErrorBar($direction, 1, 1, [double]$(if ($null -ne $op.amount) { $op.amount } else { 1 }))
|
|
2937
|
+
try { $series.ErrorBars.EndStyle = $(if ($op.endStyle -eq 'none') { 0 } else { 1 }) } catch {}
|
|
2938
|
+
return [ordered]@{ op = 'set_chart_error_bars'; changed = $true; shape = [int]$op.shape; series = $seriesIndex }
|
|
2939
|
+
}
|
|
2940
|
+
'set_chart_data_labels' {
|
|
2941
|
+
$slide = Ppt-Slide $presentation $op
|
|
2942
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2943
|
+
if (-not $shape.HasChart) { throw "Shape $($op.shape) is not a chart" }
|
|
2944
|
+
$series = $shape.Chart.SeriesCollection().Item([int]$op.series)
|
|
2945
|
+
$null = $series.ApplyDataLabels()
|
|
2946
|
+
$labels = $series.DataLabels()
|
|
2947
|
+
if ($null -ne $op.showValue) { $labels.ShowValue = [bool]$op.showValue }
|
|
2948
|
+
if ($null -ne $op.showCategoryName) { $labels.ShowCategoryName = [bool]$op.showCategoryName }
|
|
2949
|
+
if ($op.numberFormat) { $labels.NumberFormat = [string]$op.numberFormat }
|
|
2950
|
+
if ($null -ne $op.position) { $labels.Position = [int]$op.position }
|
|
2951
|
+
return [ordered]@{ op = 'set_chart_data_labels'; changed = $true; shape = [int]$op.shape; series = [int]$op.series }
|
|
2952
|
+
}
|
|
2953
|
+
'fit_text' {
|
|
2954
|
+
$slide = Ppt-Slide $presentation $op
|
|
2955
|
+
$shape = $slide.Shapes.Item([int]$op.shape)
|
|
2956
|
+
$minimum = if ($op.minFontSize) { [single]$op.minFontSize } else { [single]8 }
|
|
2957
|
+
try { $shape.TextFrame.AutoSize = 0 } catch {}
|
|
2958
|
+
try { $shape.TextFrame2.AutoSize = 0 } catch {}
|
|
2959
|
+
$maximumWidth = [single]$presentation.PageSetup.SlideWidth - [single]$shape.Left
|
|
2960
|
+
$maximumHeight = [single]$presentation.PageSetup.SlideHeight - [single]$shape.Top
|
|
2961
|
+
if ($shape.Width -gt $maximumWidth) { $shape.Width = $maximumWidth }
|
|
2962
|
+
if ($shape.Height -gt $maximumHeight) { $shape.Height = $maximumHeight }
|
|
2963
|
+
$font = $shape.TextFrame.TextRange.Font
|
|
2964
|
+
while ($font.Size -gt $minimum -and (
|
|
2965
|
+
[single]$shape.TextFrame2.TextRange.BoundWidth -gt ([single]$shape.Width + 1) -or
|
|
2966
|
+
[single]$shape.TextFrame2.TextRange.BoundHeight -gt ([single]$shape.Height + 1)
|
|
2967
|
+
)) {
|
|
2968
|
+
$font.Size = [single]$font.Size - 1
|
|
2969
|
+
}
|
|
2970
|
+
return [ordered]@{ op = 'fit_text'; changed = $true; shape = [int]$op.shape; fontSize = [single]$font.Size }
|
|
2971
|
+
}
|
|
2972
|
+
default { throw "Unsupported PPTX operation: $($op.op)" }
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function Apply-Operations($document, [string]$format, $operations, [bool]$live, [bool]$requireChanges = $true) {
|
|
2977
|
+
$results = @()
|
|
2978
|
+
$undoUnits = 0
|
|
2979
|
+
$wordUndoRecord = $null
|
|
2980
|
+
$wordRecordStarted = $false
|
|
2981
|
+
$failure = $null
|
|
2982
|
+
$beforeFingerprint = ''
|
|
2983
|
+
if ($live -and @('docx', 'pptx') -contains $format) {
|
|
2984
|
+
$beforeFingerprint = Snapshot-Fingerprint (Snapshot-Document $document $format ([ordered]@{}))
|
|
2985
|
+
}
|
|
2986
|
+
if ($live -and $format -eq 'docx') {
|
|
2987
|
+
try {
|
|
2988
|
+
$wordUndoRecord = $document.Application.UndoRecord
|
|
2989
|
+
$wordUndoRecord.StartCustomRecord('Mixdog Office transaction')
|
|
2990
|
+
$wordRecordStarted = $true
|
|
2991
|
+
} catch {}
|
|
2992
|
+
}
|
|
2993
|
+
try {
|
|
2994
|
+
foreach ($op in @($operations)) {
|
|
2995
|
+
if ($live -and $format -eq 'pptx') {
|
|
2996
|
+
try { $document.Application.StartNewUndoEntry() } catch {}
|
|
2997
|
+
}
|
|
2998
|
+
$emitted = @(switch ($format) {
|
|
2999
|
+
'docx' { Apply-WordOperation $document $op }
|
|
3000
|
+
'xlsx' { Apply-ExcelOperation $document $op }
|
|
3001
|
+
'pptx' { Apply-PowerPointOperation $document $op $live }
|
|
3002
|
+
})
|
|
3003
|
+
$structured = @($emitted | Where-Object { $_ -is [System.Collections.IDictionary] } | Select-Object -Last 1)
|
|
3004
|
+
if ($structured.Count -ne 1) { throw "$($op.op) returned no structured operation result" }
|
|
3005
|
+
$entry = $structured[0]
|
|
3006
|
+
if ($requireChanges -and -not [bool]$op.allowNoChange -and $entry.Contains('changed') -and -not [bool]$entry.changed) {
|
|
3007
|
+
throw "$($op.op) produced no change"
|
|
3008
|
+
}
|
|
3009
|
+
$results += $entry
|
|
3010
|
+
if ($live -and $format -eq 'pptx') { $undoUnits++ }
|
|
3011
|
+
}
|
|
3012
|
+
} catch {
|
|
3013
|
+
$line = [int]$_.InvocationInfo.ScriptLineNumber
|
|
3014
|
+
$failure = "$($op.op) failed at office-com-host.ps1:$line`: $($_.Exception.Message)"
|
|
3015
|
+
} finally {
|
|
3016
|
+
if ($wordRecordStarted) {
|
|
3017
|
+
try { $wordUndoRecord.EndCustomRecord() } catch {}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
if ($failure) {
|
|
3021
|
+
if ($live -and $format -eq 'docx' -and $wordRecordStarted) {
|
|
3022
|
+
try { $null = $document.Undo(1) } catch {}
|
|
3023
|
+
} elseif ($live -and $format -eq 'pptx') {
|
|
3024
|
+
$attempts = 0
|
|
3025
|
+
$maximumAttempts = @($operations).Count + 5
|
|
3026
|
+
while ($attempts -lt $maximumAttempts) {
|
|
3027
|
+
$currentFingerprint = Snapshot-Fingerprint (Snapshot-Document $document $format ([ordered]@{}))
|
|
3028
|
+
if ($currentFingerprint -eq $beforeFingerprint) { break }
|
|
3029
|
+
try { $document.Application.CommandBars.ExecuteMso('Undo') } catch { break }
|
|
3030
|
+
$attempts++
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
throw $failure
|
|
3034
|
+
}
|
|
3035
|
+
if ($live -and $format -eq 'docx' -and @($operations).Count -gt 0) { $undoUnits = 1 }
|
|
3036
|
+
if ($live -and $format -eq 'xlsx' -and @($operations).Count -gt 0) { $undoUnits = 1 }
|
|
3037
|
+
return [ordered]@{ results = $results; undoUnits = $undoUnits }
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function Office-Issue([string]$severity, [string]$code, [string]$path, [string]$message) {
|
|
3041
|
+
return [ordered]@{ severity = $severity; code = $code; path = $path; message = $message }
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
function Installed-OfficeFonts {
|
|
3045
|
+
$fonts = @{}
|
|
3046
|
+
try {
|
|
3047
|
+
Add-Type -AssemblyName System.Drawing
|
|
3048
|
+
$collection = [System.Drawing.Text.InstalledFontCollection]::new()
|
|
3049
|
+
foreach ($family in @($collection.Families)) { $fonts[[string]$family.Name] = $true }
|
|
3050
|
+
$collection.Dispose()
|
|
3051
|
+
} catch {}
|
|
3052
|
+
return $fonts
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
function Missing-FontIssue($fonts, [string]$name, [string]$path) {
|
|
3056
|
+
if ([string]::IsNullOrWhiteSpace($name) -or $name.StartsWith('+') -or $fonts.ContainsKey($name)) { return $null }
|
|
3057
|
+
return Office-Issue 'warning' 'missing_font' $path "Font is not installed: $name"
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function Excel-MatrixValue($matrix, [int]$row, [int]$column) {
|
|
3061
|
+
if ($matrix -is [Array] -and $matrix.Rank -eq 2) {
|
|
3062
|
+
return $matrix.GetValue(
|
|
3063
|
+
$matrix.GetLowerBound(0) + $row - 1,
|
|
3064
|
+
$matrix.GetLowerBound(1) + $column - 1
|
|
3065
|
+
)
|
|
3066
|
+
}
|
|
3067
|
+
if ($row -eq 1 -and $column -eq 1) { return $matrix }
|
|
3068
|
+
return $null
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function Excel-ColumnLetters([int]$column) {
|
|
3072
|
+
$letters = ''
|
|
3073
|
+
while ($column -gt 0) {
|
|
3074
|
+
$column--
|
|
3075
|
+
$letters = [char](65 + ($column % 26)) + $letters
|
|
3076
|
+
$column = [Math]::Floor($column / 26)
|
|
3077
|
+
}
|
|
3078
|
+
return $letters
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function Excel-RangeCellAddress($range, [int]$row, [int]$column) {
|
|
3082
|
+
$absoluteRow = [int]$range.Row + $row - 1
|
|
3083
|
+
$absoluteColumn = [int]$range.Column + $column - 1
|
|
3084
|
+
return "$(Excel-ColumnLetters $absoluteColumn)$absoluteRow"
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
function Issues-Word($doc) {
|
|
3088
|
+
$issues = @()
|
|
3089
|
+
$fonts = Installed-OfficeFonts
|
|
3090
|
+
if ($doc.Revisions.Count -gt 0) { $issues += Office-Issue 'info' 'unresolved_revisions' '/body' "$($doc.Revisions.Count) tracked revision(s) remain unresolved." }
|
|
3091
|
+
if ($doc.Comments.Count -gt 0) { $issues += Office-Issue 'info' 'unresolved_comments' '/body' "$($doc.Comments.Count) comment(s) remain in the document." }
|
|
3092
|
+
$fieldIndex = 0
|
|
3093
|
+
foreach ($field in @($doc.Fields)) {
|
|
3094
|
+
$fieldIndex++
|
|
3095
|
+
try {
|
|
3096
|
+
$text = ([string]$field.Result.Text).Trim()
|
|
3097
|
+
if ($text -match 'Error!|Reference source not found') { $issues += Office-Issue 'error' 'field_error' "/body/field[$fieldIndex]" $text }
|
|
3098
|
+
} catch {}
|
|
3099
|
+
}
|
|
3100
|
+
for ($paragraphIndex = 1; $paragraphIndex -le $doc.Paragraphs.Count; $paragraphIndex++) {
|
|
3101
|
+
try {
|
|
3102
|
+
$paragraph = $doc.Paragraphs.Item($paragraphIndex)
|
|
3103
|
+
$path = "/body/p[$paragraphIndex]"
|
|
3104
|
+
$fontIssue = Missing-FontIssue $fonts ([string]$paragraph.Range.Font.Name) $path
|
|
3105
|
+
if ($fontIssue) { $issues += $fontIssue }
|
|
3106
|
+
$text = ([string]$paragraph.Range.Text).TrimEnd("`r", "`a")
|
|
3107
|
+
$fontSize = [single]$paragraph.Range.Font.Size
|
|
3108
|
+
if ($text.Length -gt 180 -and $fontSize -ge 18) {
|
|
3109
|
+
$issues += Office-Issue 'warning' 'oversized_heading_text' $path 'A long paragraph uses heading-sized text and is likely mis-styled.'
|
|
3110
|
+
}
|
|
3111
|
+
} catch {}
|
|
3112
|
+
}
|
|
3113
|
+
for ($tableIndex = 1; $tableIndex -le $doc.Tables.Count; $tableIndex++) {
|
|
3114
|
+
try {
|
|
3115
|
+
$table = $doc.Tables.Item($tableIndex)
|
|
3116
|
+
$width = 0
|
|
3117
|
+
foreach ($column in @($table.Columns)) { $width += [double]$column.Width }
|
|
3118
|
+
$page = $table.Range.Sections.Item(1).PageSetup
|
|
3119
|
+
$available = [double]$page.PageWidth - [double]$page.LeftMargin - [double]$page.RightMargin
|
|
3120
|
+
if ($width -gt ($available + 1)) {
|
|
3121
|
+
$issues += Office-Issue 'warning' 'table_width' "/body/tbl[$tableIndex]" "Table width $([Math]::Round($width, 1))pt exceeds available page width $([Math]::Round($available, 1))pt."
|
|
3122
|
+
}
|
|
3123
|
+
} catch {}
|
|
3124
|
+
}
|
|
3125
|
+
return $issues
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function Add-CappedOfficeIssue($issues, [ref]$omitted, $issue, [int]$limit = 500) {
|
|
3129
|
+
if ($issues.Count -lt $limit) {
|
|
3130
|
+
[void]$issues.Add($issue)
|
|
3131
|
+
} else {
|
|
3132
|
+
$omitted.Value = [int]$omitted.Value + 1
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
function Inspect-ExcelFinancialRange($sheet, $used, $fonts, $commentAddresses) {
|
|
3137
|
+
$issues = [System.Collections.ArrayList]::new()
|
|
3138
|
+
$omitted = 0
|
|
3139
|
+
$rowCount = [int]$used.Rows.Count
|
|
3140
|
+
$columnCount = [int]$used.Columns.Count
|
|
3141
|
+
$totalCells = [int64]$rowCount * [int64]$columnCount
|
|
3142
|
+
$targetChunkCells = 20000
|
|
3143
|
+
$rowsPerBlock = [Math]::Min(2000, [Math]::Max(1, [Math]::Floor($targetChunkCells / [Math]::Max(1, [Math]::Min($columnCount, $targetChunkCells)))))
|
|
3144
|
+
$scannedCells = [int64]0
|
|
3145
|
+
$chunks = 0
|
|
3146
|
+
$visualProbeCount = 0
|
|
3147
|
+
$checksSheet = [string]::Equals([string]$sheet.Name, 'Checks', [System.StringComparison]::OrdinalIgnoreCase)
|
|
3148
|
+
|
|
3149
|
+
for ($rowStart = 1; $rowStart -le $rowCount; $rowStart += $rowsPerBlock) {
|
|
3150
|
+
$rowsThisBlock = [Math]::Min($rowsPerBlock, $rowCount - $rowStart + 1)
|
|
3151
|
+
$states = @()
|
|
3152
|
+
for ($localRow = 1; $localRow -le $rowsThisBlock; $localRow++) {
|
|
3153
|
+
$states += [pscustomobject]@{
|
|
3154
|
+
FormulaCount = 0
|
|
3155
|
+
FirstFormula = [int]::MaxValue
|
|
3156
|
+
LastFormula = 0
|
|
3157
|
+
Values = [System.Collections.ArrayList]::new()
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
$columnsPerBlock = [Math]::Max(1, [Math]::Floor($targetChunkCells / $rowsThisBlock))
|
|
3161
|
+
for ($columnStart = 1; $columnStart -le $columnCount; $columnStart += $columnsPerBlock) {
|
|
3162
|
+
$columnsThisBlock = [Math]::Min($columnsPerBlock, $columnCount - $columnStart + 1)
|
|
3163
|
+
$chunk = $used.Cells.Item($rowStart, $columnStart).Resize($rowsThisBlock, $columnsThisBlock)
|
|
3164
|
+
$values = $chunk.Value2
|
|
3165
|
+
$formulas = $chunk.FormulaR1C1
|
|
3166
|
+
$chunks++
|
|
3167
|
+
$scannedCells += [int64]$rowsThisBlock * [int64]$columnsThisBlock
|
|
3168
|
+
for ($localRow = 1; $localRow -le $rowsThisBlock; $localRow++) {
|
|
3169
|
+
$state = $states[$localRow - 1]
|
|
3170
|
+
for ($localColumn = 1; $localColumn -le $columnsThisBlock; $localColumn++) {
|
|
3171
|
+
$value = Excel-MatrixValue $values $localRow $localColumn
|
|
3172
|
+
$formula = Excel-MatrixValue $formulas $localRow $localColumn
|
|
3173
|
+
$hasFormula = [string]$formula -match '^='
|
|
3174
|
+
$absoluteRow = [int]$used.Row + $rowStart + $localRow - 2
|
|
3175
|
+
$absoluteColumn = [int]$used.Column + $columnStart + $localColumn - 2
|
|
3176
|
+
$address = "$(Excel-ColumnLetters $absoluteColumn)$absoluteRow"
|
|
3177
|
+
$path = "/sheet[$($sheet.Name)]/cell[$address]"
|
|
3178
|
+
if ($hasFormula) {
|
|
3179
|
+
$state.FormulaCount++
|
|
3180
|
+
$state.FirstFormula = [Math]::Min([int]$state.FirstFormula, $absoluteColumn)
|
|
3181
|
+
$state.LastFormula = [Math]::Max([int]$state.LastFormula, $absoluteColumn)
|
|
3182
|
+
} elseif ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) {
|
|
3183
|
+
[void]$state.Values.Add([pscustomobject]@{
|
|
3184
|
+
Address = $address
|
|
3185
|
+
Path = $path
|
|
3186
|
+
Column = $absoluteColumn
|
|
3187
|
+
Numeric = $value -is [ValueType] -and $value -isnot [bool]
|
|
3188
|
+
})
|
|
3189
|
+
}
|
|
3190
|
+
if ($checksSheet -and $value -is [bool] -and -not [bool]$value) {
|
|
3191
|
+
Add-CappedOfficeIssue $issues ([ref]$omitted) (Office-Issue 'error' 'failed_check' $path 'A formula or explicit check on the Checks sheet evaluates to FALSE.')
|
|
3192
|
+
}
|
|
3193
|
+
if (($null -ne $value -or $hasFormula) -and $visualProbeCount -lt 32) {
|
|
3194
|
+
$visualProbeCount++
|
|
3195
|
+
$cell = $chunk.Cells.Item($localRow, $localColumn)
|
|
3196
|
+
if ([string]$cell.Text -match '^#{3,}$') {
|
|
3197
|
+
Add-CappedOfficeIssue $issues ([ref]$omitted) (Office-Issue 'warning' 'cell_overflow' $path 'Displayed value is clipped because the column is too narrow.')
|
|
3198
|
+
}
|
|
3199
|
+
$fontIssue = Missing-FontIssue $fonts ([string]$cell.Font.Name) $path
|
|
3200
|
+
if ($fontIssue) { Add-CappedOfficeIssue $issues ([ref]$omitted) $fontIssue }
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
foreach ($state in $states) {
|
|
3206
|
+
foreach ($entry in @($state.Values)) {
|
|
3207
|
+
if ($entry.Numeric) {
|
|
3208
|
+
if (-not $commentAddresses.ContainsKey([string]$entry.Address)) {
|
|
3209
|
+
Add-CappedOfficeIssue $issues ([ref]$omitted) (Office-Issue 'warning' 'hardcode_missing_source' ([string]$entry.Path) 'Hardcoded numeric input has no source comment.')
|
|
3210
|
+
}
|
|
3211
|
+
if ([int]$state.FormulaCount -gt 0) {
|
|
3212
|
+
Add-CappedOfficeIssue $issues ([ref]$omitted) (Office-Issue 'warning' 'rogue_hardcode' ([string]$entry.Path) 'Numeric hardcode appears inside a row that otherwise contains formulas.')
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if ([int]$state.FormulaCount -ge 3 -and [int]$entry.Column -gt [int]$state.FirstFormula -and [int]$entry.Column -lt [int]$state.LastFormula) {
|
|
3216
|
+
Add-CappedOfficeIssue $issues ([ref]$omitted) (Office-Issue 'warning' 'formula_inconsistency' ([string]$entry.Path) 'A hardcoded value interrupts a row containing three or more formulas.')
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
if ($omitted -gt 0) {
|
|
3222
|
+
[void]$issues.Add((Office-Issue 'warning' 'audit_issues_truncated' "/sheet[$($sheet.Name)]" "$omitted additional financial-model audit issue(s) were omitted after the 500-issue response cap."))
|
|
3223
|
+
}
|
|
3224
|
+
return [ordered]@{
|
|
3225
|
+
issues = [object[]]$issues.ToArray()
|
|
3226
|
+
coverage = [ordered]@{
|
|
3227
|
+
sheet = [string]$sheet.Name
|
|
3228
|
+
range = [string]$used.Address($false, $false)
|
|
3229
|
+
rows = $rowCount
|
|
3230
|
+
columns = $columnCount
|
|
3231
|
+
totalCells = $totalCells
|
|
3232
|
+
scannedCells = $scannedCells
|
|
3233
|
+
chunks = $chunks
|
|
3234
|
+
complete = $scannedCells -eq $totalCells
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
function Issues-Excel($book, $payload) {
|
|
3240
|
+
$issues = @()
|
|
3241
|
+
$coverage = @()
|
|
3242
|
+
$fonts = Installed-OfficeFonts
|
|
3243
|
+
$financialAudit = $payload.auditProfile -eq 'financial-model'
|
|
3244
|
+
if ($financialAudit) {
|
|
3245
|
+
$hasChecks = $false
|
|
3246
|
+
foreach ($candidate in @($book.Worksheets)) {
|
|
3247
|
+
if ([string]::Equals([string]$candidate.Name, 'Checks', [System.StringComparison]::OrdinalIgnoreCase)) { $hasChecks = $true; break }
|
|
3248
|
+
}
|
|
3249
|
+
if (-not $hasChecks) { $issues += Office-Issue 'warning' 'missing_checks_sheet' '/' 'Financial-model audit expects a Checks sheet with explicit tie-out formulas.' }
|
|
3250
|
+
}
|
|
3251
|
+
foreach ($sheet in @($book.Worksheets)) {
|
|
3252
|
+
if ($payload.sheet -and -not [string]::Equals([string]$payload.sheet, [string]$sheet.Name, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
|
|
3253
|
+
$used = $sheet.UsedRange
|
|
3254
|
+
if ($payload.range) { $used = $sheet.Range([string]$payload.range) }
|
|
3255
|
+
if ($financialAudit) {
|
|
3256
|
+
$commentAddresses = @{}
|
|
3257
|
+
try {
|
|
3258
|
+
$commentCells = $used.SpecialCells(-4144)
|
|
3259
|
+
foreach ($commentCell in @($commentCells.Cells)) {
|
|
3260
|
+
$commentAddresses[[string]$commentCell.Address($false, $false)] = $true
|
|
3261
|
+
}
|
|
3262
|
+
} catch {}
|
|
3263
|
+
$inspection = Inspect-ExcelFinancialRange $sheet $used $fonts $commentAddresses
|
|
3264
|
+
$issues += @($inspection.issues)
|
|
3265
|
+
$coverage += $inspection.coverage
|
|
3266
|
+
} else {
|
|
3267
|
+
$columns = [Math]::Min([int]$used.Columns.Count, 100)
|
|
3268
|
+
$sampleRows = [Math]::Min([int]$used.Rows.Count, [Math]::Max(1, [Math]::Floor(500 / [Math]::Max(1, $columns))))
|
|
3269
|
+
$sample = $used.Resize($sampleRows, $columns)
|
|
3270
|
+
$sampleValues = $sample.Value2
|
|
3271
|
+
$sampleFormulas = $sample.FormulaR1C1
|
|
3272
|
+
$visualProbeCount = 0
|
|
3273
|
+
for ($r = 1; $r -le $sampleRows; $r++) {
|
|
3274
|
+
for ($c = 1; $c -le $columns; $c++) {
|
|
3275
|
+
$value = Excel-MatrixValue $sampleValues $r $c
|
|
3276
|
+
$formula = Excel-MatrixValue $sampleFormulas $r $c
|
|
3277
|
+
if (($null -ne $value -or [string]$formula -match '^=') -and $visualProbeCount -lt 32) {
|
|
3278
|
+
$visualProbeCount++
|
|
3279
|
+
$cell = $sample.Cells.Item($r, $c)
|
|
3280
|
+
$path = "/sheet[$($sheet.Name)]/cell[$(Excel-RangeCellAddress $used $r $c)]"
|
|
3281
|
+
if ([string]$cell.Text -match '^#{3,}$') { $issues += Office-Issue 'warning' 'cell_overflow' $path 'Displayed value is clipped because the column is too narrow.' }
|
|
3282
|
+
$fontIssue = Missing-FontIssue $fonts ([string]$cell.Font.Name) $path
|
|
3283
|
+
if ($fontIssue) { $issues += $fontIssue }
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
$coverage += [ordered]@{
|
|
3288
|
+
sheet = [string]$sheet.Name
|
|
3289
|
+
range = [string]$used.Address($false, $false)
|
|
3290
|
+
rows = [int]$used.Rows.Count
|
|
3291
|
+
columns = [int]$used.Columns.Count
|
|
3292
|
+
totalCells = [int64]$used.Rows.Count * [int64]$used.Columns.Count
|
|
3293
|
+
scannedCells = [int64]$sampleRows * [int64]$columns
|
|
3294
|
+
complete = [int64]$sampleRows * [int64]$columns -eq [int64]$used.Rows.Count * [int64]$used.Columns.Count
|
|
3295
|
+
}
|
|
3296
|
+
try {
|
|
3297
|
+
$formulaMatrix = $used.FormulaR1C1
|
|
3298
|
+
$inconsistencies = 0
|
|
3299
|
+
if ($formulaMatrix -is [Array] -and $formulaMatrix.Rank -eq 2) {
|
|
3300
|
+
$rowLower = $formulaMatrix.GetLowerBound(0)
|
|
3301
|
+
$rowUpper = $formulaMatrix.GetUpperBound(0)
|
|
3302
|
+
$columnLower = $formulaMatrix.GetLowerBound(1)
|
|
3303
|
+
$columnUpper = $formulaMatrix.GetUpperBound(1)
|
|
3304
|
+
for ($matrixRow = $rowLower; $matrixRow -le $rowUpper -and $inconsistencies -lt 100; $matrixRow++) {
|
|
3305
|
+
$formulaColumns = @()
|
|
3306
|
+
for ($matrixColumn = $columnLower; $matrixColumn -le $columnUpper; $matrixColumn++) {
|
|
3307
|
+
if ([string]$formulaMatrix.GetValue($matrixRow, $matrixColumn) -match '^=') { $formulaColumns += $matrixColumn }
|
|
3308
|
+
}
|
|
3309
|
+
if ($formulaColumns.Count -lt 3) { continue }
|
|
3310
|
+
$firstFormula = ($formulaColumns | Measure-Object -Minimum).Minimum
|
|
3311
|
+
$lastFormula = ($formulaColumns | Measure-Object -Maximum).Maximum
|
|
3312
|
+
for ($matrixColumn = $firstFormula; $matrixColumn -le $lastFormula -and $inconsistencies -lt 100; $matrixColumn++) {
|
|
3313
|
+
$entry = [string]$formulaMatrix.GetValue($matrixRow, $matrixColumn)
|
|
3314
|
+
if (-not [string]::IsNullOrWhiteSpace($entry) -and $entry -notmatch '^=') {
|
|
3315
|
+
$cell = $used.Cells.Item($matrixRow - $rowLower + 1, $matrixColumn - $columnLower + 1)
|
|
3316
|
+
$issues += Office-Issue 'warning' 'formula_inconsistency' "/sheet[$($sheet.Name)]/cell[$($cell.Address($false, $false))]" 'A hardcoded value interrupts a row containing three or more formulas.'
|
|
3317
|
+
$inconsistencies++
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
} catch {}
|
|
3323
|
+
}
|
|
3324
|
+
try {
|
|
3325
|
+
$errorCells = $used.SpecialCells(-4123, 16)
|
|
3326
|
+
$errorCount = [int]$errorCells.Cells.Count
|
|
3327
|
+
$limit = [Math]::Min($errorCount, 1000)
|
|
3328
|
+
for ($errorIndex = 1; $errorIndex -le $limit; $errorIndex++) {
|
|
3329
|
+
$cell = $errorCells.Cells.Item($errorIndex)
|
|
3330
|
+
$text = [string]$cell.Text
|
|
3331
|
+
$issues += Office-Issue 'error' 'formula_error' "/sheet[$($sheet.Name)]/cell[$($cell.Address($false, $false))]" "Cell contains formula error $text"
|
|
3332
|
+
}
|
|
3333
|
+
if ($errorCount -gt $limit) {
|
|
3334
|
+
$issues += Office-Issue 'error' 'formula_error_truncated' "/sheet[$($sheet.Name)]" "$($errorCount - $limit) additional formula error(s) were omitted from the report."
|
|
3335
|
+
}
|
|
3336
|
+
} catch {}
|
|
3337
|
+
for ($chartIndex = 1; $chartIndex -le $sheet.ChartObjects().Count; $chartIndex++) {
|
|
3338
|
+
try {
|
|
3339
|
+
$chart = $sheet.ChartObjects().Item($chartIndex).Chart
|
|
3340
|
+
if ($chart.SeriesCollection().Count -eq 0) {
|
|
3341
|
+
$issues += Office-Issue 'error' 'empty_chart' "/sheet[$($sheet.Name)]/chart[$chartIndex]" 'Chart has no data series.'
|
|
3342
|
+
}
|
|
3343
|
+
} catch {
|
|
3344
|
+
$issues += Office-Issue 'error' 'broken_chart' "/sheet[$($sheet.Name)]/chart[$chartIndex]" 'Chart data could not be inspected.'
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
try {
|
|
3349
|
+
$links = @($book.LinkSources(1))
|
|
3350
|
+
foreach ($link in $links) { if ($link) { $issues += Office-Issue 'warning' 'external_link' '/' "Workbook links to external source: $link" } }
|
|
3351
|
+
} catch {}
|
|
3352
|
+
$totalCells = [int64]0
|
|
3353
|
+
$scannedCells = [int64]0
|
|
3354
|
+
foreach ($entry in $coverage) {
|
|
3355
|
+
$totalCells += [int64]$entry.totalCells
|
|
3356
|
+
$scannedCells += [int64]$entry.scannedCells
|
|
3357
|
+
}
|
|
3358
|
+
return [ordered]@{
|
|
3359
|
+
issues = @($issues)
|
|
3360
|
+
auditCoverage = [ordered]@{
|
|
3361
|
+
mode = $(if ($financialAudit) { 'full' } else { 'sampled' })
|
|
3362
|
+
complete = @($coverage | Where-Object { -not $_.complete }).Count -eq 0
|
|
3363
|
+
totalCells = $totalCells
|
|
3364
|
+
scannedCells = $scannedCells
|
|
3365
|
+
sheets = $coverage
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
function Ole-ColorChannel([long]$color, [int]$shift) {
|
|
3371
|
+
return [double](($color -shr $shift) -band 255) / 255
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function Color-Luminance([long]$color) {
|
|
3375
|
+
$channels = @(
|
|
3376
|
+
(Ole-ColorChannel $color 0),
|
|
3377
|
+
(Ole-ColorChannel $color 8),
|
|
3378
|
+
(Ole-ColorChannel $color 16)
|
|
3379
|
+
)
|
|
3380
|
+
$linear = @()
|
|
3381
|
+
foreach ($channel in $channels) {
|
|
3382
|
+
$linear += $(if ($channel -le 0.03928) { $channel / 12.92 } else { [Math]::Pow(($channel + 0.055) / 1.055, 2.4) })
|
|
3383
|
+
}
|
|
3384
|
+
return (0.2126 * $linear[0]) + (0.7152 * $linear[1]) + (0.0722 * $linear[2])
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
function Color-ContrastRatio([long]$left, [long]$right) {
|
|
3388
|
+
$leftLuminance = Color-Luminance $left
|
|
3389
|
+
$rightLuminance = Color-Luminance $right
|
|
3390
|
+
$lighter = [Math]::Max($leftLuminance, $rightLuminance)
|
|
3391
|
+
$darker = [Math]::Min($leftLuminance, $rightLuminance)
|
|
3392
|
+
return ($lighter + 0.05) / ($darker + 0.05)
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
function Issues-PowerPoint($presentation, $payload) {
|
|
3396
|
+
$issues = @()
|
|
3397
|
+
$fonts = Installed-OfficeFonts
|
|
3398
|
+
foreach ($slide in @($presentation.Slides)) {
|
|
3399
|
+
if ($payload.pages -and -not (@($payload.pages | ForEach-Object { [int]$_ }) -contains [int]$slide.SlideIndex)) { continue }
|
|
3400
|
+
$shapeIndex = 0
|
|
3401
|
+
$textShapeCount = 0
|
|
3402
|
+
$visualShapeCount = 0
|
|
3403
|
+
foreach ($shape in @($slide.Shapes)) {
|
|
3404
|
+
$shapeIndex++
|
|
3405
|
+
$path = "/slide[$($slide.SlideIndex)]/shape[$shapeIndex]"
|
|
3406
|
+
try {
|
|
3407
|
+
if (@(1, 3, 6, 13, 21, 28) -contains [int]$shape.Type -or $shape.HasChart -or $shape.HasTable) { $visualShapeCount++ }
|
|
3408
|
+
} catch {}
|
|
3409
|
+
try {
|
|
3410
|
+
if ($shape.Type -eq 13 -and [string]::IsNullOrWhiteSpace([string]$shape.AlternativeText)) {
|
|
3411
|
+
$issues += Office-Issue 'warning' 'missing_alt_text' $path 'Picture has no alternative text.'
|
|
3412
|
+
}
|
|
3413
|
+
} catch {}
|
|
3414
|
+
try {
|
|
3415
|
+
if ($shape.HasTextFrame -and $shape.TextFrame.HasText) {
|
|
3416
|
+
$textShapeCount++
|
|
3417
|
+
$boundWidth = [single]$shape.TextFrame2.TextRange.BoundWidth
|
|
3418
|
+
$boundHeight = [single]$shape.TextFrame2.TextRange.BoundHeight
|
|
3419
|
+
if ($boundWidth -gt ([single]$shape.Width + 1) -or $boundHeight -gt ([single]$shape.Height + 1)) {
|
|
3420
|
+
$issues += Office-Issue 'warning' 'text_overflow' $path 'Text bounds exceed the containing shape.'
|
|
3421
|
+
}
|
|
3422
|
+
$fontIssue = Missing-FontIssue $fonts ([string]$shape.TextFrame.TextRange.Font.Name) $path
|
|
3423
|
+
if ($fontIssue) { $issues += $fontIssue }
|
|
3424
|
+
$fontSize = [single]$shape.TextFrame.TextRange.Font.Size
|
|
3425
|
+
if ($fontSize -gt 0 -and $fontSize -lt 12) {
|
|
3426
|
+
$issues += Office-Issue 'warning' 'small_font' $path "Text uses $fontSize pt; presentation body text should normally be at least 12 pt."
|
|
3427
|
+
}
|
|
3428
|
+
$slideWidth = [single]$presentation.PageSetup.SlideWidth
|
|
3429
|
+
$slideHeight = [single]$presentation.PageSetup.SlideHeight
|
|
3430
|
+
if ($shape.Left -lt 0 -or $shape.Top -lt 0 -or
|
|
3431
|
+
([single]$shape.Left + [single]$shape.Width) -gt ($slideWidth + 1) -or
|
|
3432
|
+
([single]$shape.Top + [single]$shape.Height) -gt ($slideHeight + 1)) {
|
|
3433
|
+
$issues += Office-Issue 'warning' 'text_outside_slide' $path 'Text shape extends outside the slide boundary.'
|
|
3434
|
+
}
|
|
3435
|
+
if ($shape.Left -lt 18 -or $shape.Top -lt 18 -or
|
|
3436
|
+
([single]$shape.Left + [single]$shape.Width) -gt ($slideWidth - 18) -or
|
|
3437
|
+
([single]$shape.Top + [single]$shape.Height) -gt ($slideHeight - 18)) {
|
|
3438
|
+
$issues += Office-Issue 'warning' 'edge_margin' $path 'Text is within 18 pt of a slide edge.'
|
|
3439
|
+
}
|
|
3440
|
+
try {
|
|
3441
|
+
if ($shape.Fill.Visible -and [long]$shape.Fill.ForeColor.RGB -ge 0 -and [long]$shape.TextFrame.TextRange.Font.Color.RGB -ge 0) {
|
|
3442
|
+
$contrast = Color-ContrastRatio ([long]$shape.TextFrame.TextRange.Font.Color.RGB) ([long]$shape.Fill.ForeColor.RGB)
|
|
3443
|
+
if ($contrast -lt 3) {
|
|
3444
|
+
$issues += Office-Issue 'warning' 'low_contrast' $path "Text-to-fill contrast ratio is $([Math]::Round($contrast, 2)):1."
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
} catch {}
|
|
3448
|
+
}
|
|
3449
|
+
} catch {}
|
|
3450
|
+
try {
|
|
3451
|
+
if ($shape.Type -eq 14 -and -not ($shape.HasTextFrame -and $shape.TextFrame.HasText)) {
|
|
3452
|
+
$placeholderType = [int]$shape.PlaceholderFormat.Type
|
|
3453
|
+
if (@(1, 2, 3, 4, 5, 6, 7) -contains $placeholderType) {
|
|
3454
|
+
$issues += Office-Issue 'warning' 'empty_placeholder' $path 'A visible content placeholder is still empty.'
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
} catch {}
|
|
3458
|
+
try {
|
|
3459
|
+
if ($shape.HasChart -and $shape.Chart.SeriesCollection().Count -eq 0) {
|
|
3460
|
+
$issues += Office-Issue 'error' 'empty_chart' "$path/chart" 'Chart has no data series.'
|
|
3461
|
+
}
|
|
3462
|
+
} catch {
|
|
3463
|
+
if ($shape.HasChart) { $issues += Office-Issue 'error' 'broken_chart' "$path/chart" 'Chart data could not be inspected.' }
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
for ($leftIndex = 1; $leftIndex -le $slide.Shapes.Count; $leftIndex++) {
|
|
3467
|
+
$left = $slide.Shapes.Item($leftIndex)
|
|
3468
|
+
for ($rightIndex = $leftIndex + 1; $rightIndex -le $slide.Shapes.Count; $rightIndex++) {
|
|
3469
|
+
$right = $slide.Shapes.Item($rightIndex)
|
|
3470
|
+
try {
|
|
3471
|
+
$leftHasText = $left.HasTextFrame -and $left.TextFrame.HasText
|
|
3472
|
+
$rightHasText = $right.HasTextFrame -and $right.TextFrame.HasText
|
|
3473
|
+
if (-not ($leftHasText -and $rightHasText)) { continue }
|
|
3474
|
+
$x = [Math]::Max(0, [Math]::Min([double]$left.Left + [double]$left.Width, [double]$right.Left + [double]$right.Width) - [Math]::Max([double]$left.Left, [double]$right.Left))
|
|
3475
|
+
$y = [Math]::Max(0, [Math]::Min([double]$left.Top + [double]$left.Height, [double]$right.Top + [double]$right.Height) - [Math]::Max([double]$left.Top, [double]$right.Top))
|
|
3476
|
+
$intersection = $x * $y
|
|
3477
|
+
$smallest = [Math]::Min([double]$left.Width * [double]$left.Height, [double]$right.Width * [double]$right.Height)
|
|
3478
|
+
if ($smallest -gt 0 -and ($intersection / $smallest) -ge 0.25) {
|
|
3479
|
+
$issues += Office-Issue 'warning' 'shape_overlap' "/slide[$($slide.SlideIndex)]" "Text shapes $leftIndex and $rightIndex overlap by at least 25%."
|
|
3480
|
+
}
|
|
3481
|
+
} catch {}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
if ($textShapeCount -gt 0 -and $visualShapeCount -eq 0) {
|
|
3485
|
+
$issues += Office-Issue 'warning' 'text_only_slide' "/slide[$($slide.SlideIndex)]" 'Slide contains text but no visual shape, chart, table, diagram, or picture.'
|
|
3486
|
+
}
|
|
3487
|
+
if ($payload.auditProfile -eq 'model-backed-deck') {
|
|
3488
|
+
$allText = @($slide.Shapes | ForEach-Object {
|
|
3489
|
+
try { if ($_.HasTextFrame -and $_.TextFrame.HasText) { [string]$_.TextFrame.TextRange.Text } } catch {}
|
|
3490
|
+
}) -join ' '
|
|
3491
|
+
$notes = $(try { [string]$slide.NotesPage.Shapes.Placeholders.Item(2).TextFrame.TextRange.Text } catch { '' })
|
|
3492
|
+
if ($allText -match '\d' -and $notes -notmatch '(?i)source\s*:|[\w .-]+!\$?[A-Z]{1,3}\$?\d+') {
|
|
3493
|
+
$issues += Office-Issue 'warning' 'number_without_source' "/slide[$($slide.SlideIndex)]" 'Model-backed deck slide contains numbers but its notes do not cite a workbook cell or source.'
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return $issues
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
function Issues-Document($document, [string]$format, $payload = $null) {
|
|
3501
|
+
if ($null -eq $payload) { $payload = [ordered]@{} }
|
|
3502
|
+
$inspection = switch ($format) {
|
|
3503
|
+
'docx' { @(Issues-Word $document) }
|
|
3504
|
+
'xlsx' { Issues-Excel $document $payload }
|
|
3505
|
+
'pptx' { @(Issues-PowerPoint $document $payload) }
|
|
3506
|
+
}
|
|
3507
|
+
$issues = if ($inspection -is [System.Collections.IDictionary] -and $inspection.Contains('issues')) { @($inspection.issues) } else { @($inspection) }
|
|
3508
|
+
$result = [ordered]@{ ok = -not (@($issues | Where-Object severity -eq 'error').Count -gt 0); format = $format; issueCount = @($issues).Count; issues = @($issues) }
|
|
3509
|
+
if ($inspection -is [System.Collections.IDictionary] -and $inspection.Contains('auditCoverage')) {
|
|
3510
|
+
$result.auditCoverage = $inspection.auditCoverage
|
|
3511
|
+
}
|
|
3512
|
+
return $result
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
function Open-ValidationDocument($app, [string]$format, [string]$path) {
|
|
3516
|
+
switch ($format) {
|
|
3517
|
+
'docx' { return $app.Documents.Open($path, $false, $true, $false) }
|
|
3518
|
+
'xlsx' { return $app.Workbooks.Open($path, 0, $true) }
|
|
3519
|
+
'pptx' { return $app.Presentations.Open($path, $true, $true, $false) }
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
function Validate-NativeDocument([string]$path, [string]$format) {
|
|
3524
|
+
$validationApp = $null
|
|
3525
|
+
$validationDocument = $null
|
|
3526
|
+
try {
|
|
3527
|
+
$validationApp = New-HiddenApplication $format (ProgId-ForFormat $format)
|
|
3528
|
+
$validationDocument = Open-ValidationDocument $validationApp $format $path
|
|
3529
|
+
$snapshot = Snapshot-Document $validationDocument $format ([ordered]@{})
|
|
3530
|
+
$inspection = Issues-Document $validationDocument $format ([ordered]@{})
|
|
3531
|
+
return [ordered]@{
|
|
3532
|
+
ok = [bool]$inspection.ok
|
|
3533
|
+
opened = $true
|
|
3534
|
+
issueCount = [int]$inspection.issueCount
|
|
3535
|
+
issues = @($inspection.issues)
|
|
3536
|
+
snapshotFingerprint = Snapshot-Fingerprint $snapshot
|
|
3537
|
+
}
|
|
3538
|
+
} catch {
|
|
3539
|
+
return [ordered]@{ ok = $false; opened = $false; error = [string]$_.Exception.Message }
|
|
3540
|
+
} finally {
|
|
3541
|
+
if ($null -ne $validationDocument) {
|
|
3542
|
+
try { $validationDocument.Close($false) } catch {}
|
|
3543
|
+
try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($validationDocument) } catch {}
|
|
3544
|
+
}
|
|
3545
|
+
if ($null -ne $validationApp) {
|
|
3546
|
+
try { $validationApp.Quit() } catch {}
|
|
3547
|
+
try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($validationApp) } catch {}
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function Save-Document($document, [string]$format) {
|
|
3553
|
+
if ($format -eq 'xlsx') {
|
|
3554
|
+
try { $document.Application.CalculateFullRebuild() } catch {}
|
|
3555
|
+
}
|
|
3556
|
+
$document.Save()
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function Save-DocumentCopy($document, [string]$format, [string]$output) {
|
|
3560
|
+
switch ($format) {
|
|
3561
|
+
'xlsx' { $document.SaveCopyAs($output) }
|
|
3562
|
+
'pptx' { $document.SaveCopyAs($output, (Office-SaveFormatForPath $format $output)) }
|
|
3563
|
+
default { throw "Save-copy is unsupported for .$format" }
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
function Text-Sha256([string]$text) {
|
|
3568
|
+
$algorithm = [System.Security.Cryptography.SHA256]::Create()
|
|
3569
|
+
try {
|
|
3570
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
|
3571
|
+
return ([BitConverter]::ToString($algorithm.ComputeHash($bytes))).Replace('-', '').ToLowerInvariant()
|
|
3572
|
+
} finally {
|
|
3573
|
+
$algorithm.Dispose()
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
function Snapshot-Fingerprint($value) {
|
|
3578
|
+
return Text-Sha256 ($value | ConvertTo-Json -Depth 30 -Compress)
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
function Restore-ExcelCheckpoint($book, [string]$checkpoint) {
|
|
3582
|
+
$app = $book.Application
|
|
3583
|
+
$alerts = $app.DisplayAlerts
|
|
3584
|
+
$app.DisplayAlerts = $false
|
|
3585
|
+
$source = $null
|
|
3586
|
+
try {
|
|
3587
|
+
$source = $app.Workbooks.Open($checkpoint, $null, $true)
|
|
3588
|
+
$originalSheets = @($book.Worksheets)
|
|
3589
|
+
$temporary = $book.Worksheets.Add()
|
|
3590
|
+
$temporary.Name = "__MixdogRestore$([guid]::NewGuid().ToString('N').Substring(0, 8))"
|
|
3591
|
+
foreach ($sheet in $originalSheets) { $sheet.Delete() }
|
|
3592
|
+
foreach ($sheet in @($source.Worksheets)) {
|
|
3593
|
+
$sheet.Copy([Type]::Missing, $book.Worksheets.Item($book.Worksheets.Count))
|
|
3594
|
+
}
|
|
3595
|
+
$temporary.Delete()
|
|
3596
|
+
$book.Saved = $false
|
|
3597
|
+
} finally {
|
|
3598
|
+
if ($null -ne $source) { try { $source.Close($false) } catch {} }
|
|
3599
|
+
$app.DisplayAlerts = $alerts
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
function Rollback-LiveDocument($document, [string]$format, [string]$checkpoint, [int]$undoUnits) {
|
|
3604
|
+
switch ($format) {
|
|
3605
|
+
'docx' {
|
|
3606
|
+
if ($undoUnits -gt 0) { $null = $document.Undo($undoUnits) }
|
|
3607
|
+
}
|
|
3608
|
+
'xlsx' { Restore-ExcelCheckpoint $document $checkpoint }
|
|
3609
|
+
'pptx' {
|
|
3610
|
+
for ($index = 0; $index -lt $undoUnits; $index++) {
|
|
3611
|
+
$document.Application.CommandBars.ExecuteMso('Undo')
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
function Render-Document($document, [string]$format, [string]$output) {
|
|
3618
|
+
switch ($format) {
|
|
3619
|
+
'docx' { $document.ExportAsFixedFormat($output, 17) }
|
|
3620
|
+
'xlsx' {
|
|
3621
|
+
$pageSetups = @()
|
|
3622
|
+
try {
|
|
3623
|
+
foreach ($sheet in @($document.Worksheets)) {
|
|
3624
|
+
try {
|
|
3625
|
+
$pageSetup = $sheet.PageSetup
|
|
3626
|
+
$pageSetups += [ordered]@{
|
|
3627
|
+
PageSetup = $pageSetup
|
|
3628
|
+
Zoom = $pageSetup.Zoom
|
|
3629
|
+
FitToPagesWide = $pageSetup.FitToPagesWide
|
|
3630
|
+
FitToPagesTall = $pageSetup.FitToPagesTall
|
|
3631
|
+
}
|
|
3632
|
+
$pageSetup.Zoom = $false
|
|
3633
|
+
$pageSetup.FitToPagesWide = 1
|
|
3634
|
+
$pageSetup.FitToPagesTall = $false
|
|
3635
|
+
} catch {}
|
|
3636
|
+
}
|
|
3637
|
+
$document.ExportAsFixedFormat(0, $output)
|
|
3638
|
+
} finally {
|
|
3639
|
+
foreach ($state in $pageSetups) {
|
|
3640
|
+
try {
|
|
3641
|
+
$state.PageSetup.Zoom = $state.Zoom
|
|
3642
|
+
$state.PageSetup.FitToPagesWide = $state.FitToPagesWide
|
|
3643
|
+
$state.PageSetup.FitToPagesTall = $state.FitToPagesTall
|
|
3644
|
+
} catch {}
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
'pptx' { $document.SaveCopyAs($output, 32) }
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
if ($env:MIXDOG_OFFICE_HOST_LIBRARY -eq '1') { return }
|
|
3653
|
+
|
|
3654
|
+
$app = $null
|
|
3655
|
+
$document = $null
|
|
3656
|
+
$live = $false
|
|
3657
|
+
$createdApp = $false
|
|
3658
|
+
try {
|
|
3659
|
+
$raw = [Console]::In.ReadToEnd()
|
|
3660
|
+
if ([string]::IsNullOrWhiteSpace($raw)) { throw 'Office host received an empty request' }
|
|
3661
|
+
$payload = $raw | ConvertFrom-Json
|
|
3662
|
+
$format = ([string]$payload.format).ToLowerInvariant()
|
|
3663
|
+
if ($payload.action -eq 'detect') {
|
|
3664
|
+
$formats = @('docx', 'xlsx', 'pptx')
|
|
3665
|
+
$items = @()
|
|
3666
|
+
foreach ($candidate in $formats) {
|
|
3667
|
+
$progId = ProgId-ForFormat $candidate
|
|
3668
|
+
$installed = Installed $progId
|
|
3669
|
+
$active = $false
|
|
3670
|
+
$open = $false
|
|
3671
|
+
if ($installed) {
|
|
3672
|
+
$candidateApp = Active-Application $progId
|
|
3673
|
+
if ($null -ne $candidateApp) {
|
|
3674
|
+
$active = $true
|
|
3675
|
+
if ($payload.path -and ($format -eq $candidate)) {
|
|
3676
|
+
$open = $null -ne (Find-OpenDocument $candidateApp $candidate ([string]$payload.path))
|
|
3677
|
+
}
|
|
3678
|
+
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($candidateApp)
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
$items += [ordered]@{ format = $candidate; installed = $installed; active = $active; documentOpen = $open }
|
|
3682
|
+
}
|
|
3683
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; available = ($items | Where-Object installed).Count -gt 0; applications = $items })
|
|
3684
|
+
exit 0
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
$progId = ProgId-ForFormat $format
|
|
3688
|
+
if (-not (Installed $progId)) { throw "Microsoft Office application for .$format is not installed" }
|
|
3689
|
+
$path = [System.IO.Path]::GetFullPath([string]$payload.path)
|
|
3690
|
+
if ($payload.mode -eq 'live') {
|
|
3691
|
+
$app = Active-Application $progId
|
|
3692
|
+
if ($null -eq $app) { throw "No running Microsoft Office application for .$format" }
|
|
3693
|
+
$document = Find-OpenDocument $app $format $path
|
|
3694
|
+
if ($null -eq $document) { throw "The exact document is not open in Microsoft Office: $path" }
|
|
3695
|
+
$live = $true
|
|
3696
|
+
} else {
|
|
3697
|
+
$app = New-HiddenApplication $format $progId
|
|
3698
|
+
$createdApp = $true
|
|
3699
|
+
$document = Open-BackgroundDocument $app $format $path
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
switch ([string]$payload.action) {
|
|
3703
|
+
'snapshot' {
|
|
3704
|
+
$value = Snapshot-Document $document $format $payload
|
|
3705
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); value = $value })
|
|
3706
|
+
}
|
|
3707
|
+
'issues' {
|
|
3708
|
+
$value = Issues-Document $document $format $payload
|
|
3709
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); value = $value })
|
|
3710
|
+
}
|
|
3711
|
+
'validate' {
|
|
3712
|
+
$value = Validate-NativeDocument $path $format
|
|
3713
|
+
$value.documentSaved = [bool]$document.Saved
|
|
3714
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); value = $value })
|
|
3715
|
+
}
|
|
3716
|
+
'checkpoint' {
|
|
3717
|
+
$output = [System.IO.Path]::GetFullPath([string]$payload.output)
|
|
3718
|
+
$value = Snapshot-Document $document $format $payload
|
|
3719
|
+
if ($format -eq 'docx') {
|
|
3720
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); fingerprint = Snapshot-Fingerprint $value; saved = [bool]$document.Saved; value = $value })
|
|
3721
|
+
} else {
|
|
3722
|
+
Save-DocumentCopy $document $format $output
|
|
3723
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); output = $output; saved = [bool]$document.Saved; value = $value })
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
'batch' {
|
|
3727
|
+
$excelCheckpoint = ''
|
|
3728
|
+
if ($live -and $format -eq 'xlsx') {
|
|
3729
|
+
$extension = [System.IO.Path]::GetExtension($path)
|
|
3730
|
+
$excelCheckpoint = Join-Path ([System.IO.Path]::GetTempPath()) "mixdog-excel-batch-$([guid]::NewGuid().ToString('N'))$extension"
|
|
3731
|
+
Save-DocumentCopy $document $format $excelCheckpoint
|
|
3732
|
+
}
|
|
3733
|
+
try {
|
|
3734
|
+
$applied = Apply-Operations $document $format $payload.operations $live ([bool]$payload.requireChanges)
|
|
3735
|
+
if (-not $live -or $payload.save) { Save-Document $document $format }
|
|
3736
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); saved = (-not $live -or [bool]$payload.save); results = $applied.results; undoUnits = $applied.undoUnits })
|
|
3737
|
+
} catch {
|
|
3738
|
+
if ($excelCheckpoint) { try { Restore-ExcelCheckpoint $document $excelCheckpoint } catch {} }
|
|
3739
|
+
throw
|
|
3740
|
+
} finally {
|
|
3741
|
+
if ($excelCheckpoint) { Remove-Item $excelCheckpoint -Force -ErrorAction SilentlyContinue }
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
'rollback' {
|
|
3745
|
+
if (-not $live) { throw 'Office COM rollback is available for live documents only' }
|
|
3746
|
+
Rollback-LiveDocument $document $format ([string]$payload.checkpoint) ([int]$payload.undoUnits)
|
|
3747
|
+
$value = Snapshot-Document $document $format $payload
|
|
3748
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = 'live'; rolledBack = $true; value = $value })
|
|
3749
|
+
}
|
|
3750
|
+
'save' {
|
|
3751
|
+
Save-Document $document $format
|
|
3752
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); saved = $true; path = $path })
|
|
3753
|
+
}
|
|
3754
|
+
'render' {
|
|
3755
|
+
$output = [System.IO.Path]::GetFullPath([string]$payload.output)
|
|
3756
|
+
$wasSaved = [bool]$document.Saved
|
|
3757
|
+
Render-Document $document $format $output
|
|
3758
|
+
if ($wasSaved -and -not [bool]$document.Saved) { $document.Saved = $true }
|
|
3759
|
+
Emit-Json ([ordered]@{ ok = $true; backend = 'microsoft-office-com'; mode = $(if ($live) { 'live' } else { 'background' }); output = $output })
|
|
3760
|
+
}
|
|
3761
|
+
default { throw "Unsupported Office host action: $($payload.action)" }
|
|
3762
|
+
}
|
|
3763
|
+
} catch {
|
|
3764
|
+
Emit-Json ([ordered]@{ ok = $false; backend = 'microsoft-office-com'; error = [string]$_.Exception.Message })
|
|
3765
|
+
exit 1
|
|
3766
|
+
} finally {
|
|
3767
|
+
if ($null -ne $document) {
|
|
3768
|
+
if (-not $live) {
|
|
3769
|
+
try { $document.Close($false) } catch {}
|
|
3770
|
+
}
|
|
3771
|
+
try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($document) } catch {}
|
|
3772
|
+
}
|
|
3773
|
+
if ($null -ne $app) {
|
|
3774
|
+
if ($createdApp) {
|
|
3775
|
+
try { $app.Quit() } catch {}
|
|
3776
|
+
}
|
|
3777
|
+
try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($app) } catch {}
|
|
3778
|
+
}
|
|
3779
|
+
[GC]::Collect()
|
|
3780
|
+
[GC]::WaitForPendingFinalizers()
|
|
3781
|
+
}
|