attention-manifest-protocol 0.4.2 → 0.4.3
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/CHANGELOG.md +341 -293
- package/CHANGELOG.zh-CN.md +41 -8
- package/README.md +250 -171
- package/README.zh-CN.md +251 -171
- package/lib/assignment/assign.d.ts +102 -0
- package/lib/assignment/assign.js +556 -0
- package/lib/assignment/binding.d.ts +103 -0
- package/lib/assignment/binding.js +575 -0
- package/lib/assignment/errors.d.ts +14 -0
- package/lib/assignment/errors.js +16 -0
- package/lib/assignment/guards.d.ts +18 -0
- package/lib/assignment/guards.js +28 -0
- package/lib/assignment/index.d.ts +10 -0
- package/lib/assignment/index.js +10 -0
- package/lib/assignment/types.d.ts +169 -0
- package/lib/assignment/types.js +1 -0
- package/lib/attention/budget.d.ts +1 -1
- package/lib/attention/budget.js +31 -9
- package/lib/bayes/engine.js +2 -1
- package/lib/bayes/reliability.js +25 -10
- package/lib/boundary/frame.d.ts +10 -4
- package/lib/boundary/frame.js +346 -109
- package/lib/boundary/kinds.d.ts +11 -0
- package/lib/boundary/kinds.js +33 -0
- package/lib/boundary/refTable.d.ts +19 -9
- package/lib/boundary/refTable.js +128 -30
- package/lib/boundary/toolCall.d.ts +22 -10
- package/lib/boundary/toolCall.js +798 -97
- package/lib/concept/conceptBudget.js +10 -1
- package/lib/concurrency/idempotency.d.ts +4 -3
- package/lib/concurrency/idempotency.js +57 -13
- package/lib/concurrency/retry.js +18 -3
- package/lib/context/agentContext.d.ts +13 -0
- package/lib/context/agentContext.js +14 -1
- package/lib/context/ast.d.ts +4 -1
- package/lib/context/ast.js +13 -3
- package/lib/context/contentStore.js +22 -9
- package/lib/context/contextBuilder.d.ts +457 -0
- package/lib/context/contextBuilder.js +1395 -0
- package/lib/context/contextPackage.d.ts +214 -0
- package/lib/context/contextPackage.js +191 -0
- package/lib/context/index.d.ts +14 -3
- package/lib/context/index.js +9 -1
- package/lib/context/relevance.d.ts +171 -0
- package/lib/context/relevance.js +250 -0
- package/lib/context/workspaceDiff.d.ts +105 -0
- package/lib/context/workspaceDiff.js +238 -0
- package/lib/context/workspaceService.d.ts +236 -1
- package/lib/context/workspaceService.js +1501 -73
- package/lib/cooperation/collaboration.d.ts +9 -0
- package/lib/cooperation/session.d.ts +4 -4
- package/lib/cooperation/session.js +3 -3
- package/lib/database/adapter/filesystemStore.d.ts +145 -0
- package/lib/database/adapter/filesystemStore.js +321 -0
- package/lib/database/adapter/index.d.ts +2 -0
- package/lib/database/adapter/index.js +2 -0
- package/lib/database/adapter/workspaceSessionStore.d.ts +118 -0
- package/lib/database/adapter/workspaceSessionStore.js +120 -0
- package/lib/database/entity/fileGraph.d.ts +53 -0
- package/lib/database/entity/fileGraph.js +15 -0
- package/lib/database/entity/fileTree.d.ts +76 -0
- package/lib/database/entity/fileTree.js +39 -0
- package/lib/database/entity/hook.d.ts +29 -0
- package/lib/database/entity/hook.js +18 -0
- package/lib/database/entity/index.d.ts +4 -0
- package/lib/database/entity/index.js +4 -0
- package/lib/database/entity/workspaceSession.d.ts +53 -0
- package/lib/database/entity/workspaceSession.js +28 -0
- package/lib/database/repository/fileAnnotationRepository.d.ts +75 -0
- package/lib/database/repository/fileAnnotationRepository.js +109 -0
- package/lib/database/repository/fileGraphRepository.d.ts +188 -0
- package/lib/database/repository/fileGraphRepository.js +648 -0
- package/lib/database/repository/fileNodeRepository.d.ts +116 -0
- package/lib/database/repository/fileNodeRepository.js +263 -0
- package/lib/database/repository/fileOutlineRepository.d.ts +40 -0
- package/lib/database/repository/fileOutlineRepository.js +104 -0
- package/lib/database/repository/fileWriteRepository.d.ts +41 -0
- package/lib/database/repository/fileWriteRepository.js +93 -0
- package/lib/database/repository/hookRepository.d.ts +67 -0
- package/lib/database/repository/hookRepository.js +150 -0
- package/lib/database/repository/index.d.ts +7 -0
- package/lib/database/repository/index.js +7 -0
- package/lib/database/repository/workspaceSessionRepository.d.ts +134 -0
- package/lib/database/repository/workspaceSessionRepository.js +327 -0
- package/lib/database/service/database.d.ts +14 -0
- package/lib/database/service/database.js +21 -0
- package/lib/database/sql/schema.js +187 -0
- package/lib/database/store/database.js +6 -1
- package/lib/delegate/team.d.ts +5 -0
- package/lib/delegate/team.js +83 -20
- package/lib/discover/classify.d.ts +20 -0
- package/lib/discover/classify.js +85 -0
- package/lib/discover/host.d.ts +20 -0
- package/lib/discover/host.js +89 -0
- package/lib/discover/index.d.ts +11 -0
- package/lib/discover/index.js +11 -0
- package/lib/discover/kinds.d.ts +14 -0
- package/lib/discover/kinds.js +23 -0
- package/lib/discover/tree.d.ts +7 -0
- package/lib/discover/tree.js +432 -0
- package/lib/discover/types.d.ts +115 -0
- package/lib/discover/types.js +1 -0
- package/lib/error/boundaryRejection.d.ts +1 -1
- package/lib/evaluation/agreement.js +2 -2
- package/lib/execution/handles.js +6 -0
- package/lib/execution/pin.js +2 -2
- package/lib/file/cfb.d.ts +56 -0
- package/lib/file/cfb.js +396 -0
- package/lib/file/dependency.d.ts +151 -0
- package/lib/file/dependency.js +568 -0
- package/lib/file/directory.d.ts +34 -0
- package/lib/file/directory.js +141 -0
- package/lib/file/errors.d.ts +20 -0
- package/lib/file/errors.js +18 -0
- package/lib/file/exportGraph.d.ts +31 -0
- package/lib/file/exportGraph.js +554 -0
- package/lib/file/file.d.ts +40 -0
- package/lib/file/file.js +339 -0
- package/lib/file/importGraph.d.ts +121 -0
- package/lib/file/importGraph.js +1022 -0
- package/lib/file/index.d.ts +45 -0
- package/lib/file/index.js +30 -0
- package/lib/file/metadata.d.ts +66 -0
- package/lib/file/metadata.js +470 -0
- package/lib/file/path.d.ts +60 -0
- package/lib/file/path.js +259 -0
- package/lib/file/read.d.ts +168 -0
- package/lib/file/read.js +541 -0
- package/lib/file/runtime.d.ts +95 -0
- package/lib/file/runtime.js +150 -0
- package/lib/file/service.d.ts +227 -0
- package/lib/file/service.js +663 -0
- package/lib/file/syntaxTree.d.ts +7 -0
- package/lib/file/syntaxTree.js +38 -0
- package/lib/file/time.d.ts +38 -0
- package/lib/file/time.js +44 -0
- package/lib/file/tree.d.ts +53 -0
- package/lib/file/tree.js +417 -0
- package/lib/file/types.d.ts +371 -0
- package/lib/file/types.js +1 -0
- package/lib/file/write.d.ts +159 -0
- package/lib/file/write.js +1028 -0
- package/lib/file/zip.d.ts +35 -0
- package/lib/file/zip.js +349 -0
- package/lib/fingerprint/algorithm.d.ts +97 -0
- package/lib/fingerprint/algorithm.js +271 -0
- package/lib/fingerprint/bytes.d.ts +18 -0
- package/lib/fingerprint/bytes.js +31 -0
- package/lib/fingerprint/error.d.ts +73 -0
- package/lib/fingerprint/error.js +86 -0
- package/lib/fingerprint/file.d.ts +71 -0
- package/lib/fingerprint/file.js +254 -0
- package/lib/fingerprint/index.d.ts +37 -0
- package/lib/fingerprint/index.js +12 -0
- package/lib/fingerprint/json.d.ts +41 -0
- package/lib/fingerprint/json.js +94 -0
- package/lib/fingerprint/number.d.ts +32 -0
- package/lib/fingerprint/number.js +95 -0
- package/lib/fingerprint/policy.d.ts +54 -0
- package/lib/fingerprint/policy.js +50 -0
- package/lib/fingerprint/preimage.d.ts +84 -0
- package/lib/fingerprint/preimage.js +133 -0
- package/lib/fingerprint/raw.d.ts +26 -0
- package/lib/fingerprint/raw.js +45 -0
- package/lib/fingerprint/ref.d.ts +39 -0
- package/lib/fingerprint/ref.js +178 -0
- package/lib/fingerprint/text.d.ts +42 -0
- package/lib/fingerprint/text.js +155 -0
- package/lib/fingerprint/tree.d.ts +56 -0
- package/lib/fingerprint/tree.js +288 -0
- package/lib/fingerprint/verify.d.ts +20 -0
- package/lib/fingerprint/verify.js +99 -0
- package/lib/global/filesystem.d.ts +43 -0
- package/lib/global/filesystem.js +75 -0
- package/lib/global/index.d.ts +8 -0
- package/lib/global/index.js +8 -0
- package/lib/global/service.d.ts +51 -0
- package/lib/global/service.js +98 -0
- package/lib/governance/transport.js +18 -6
- package/lib/hook/ast.d.ts +29 -0
- package/lib/hook/ast.js +22 -0
- package/lib/hook/code.d.ts +55 -0
- package/lib/hook/code.js +47 -0
- package/lib/hook/dependency.d.ts +45 -0
- package/lib/hook/dependency.js +28 -0
- package/lib/hook/diff.d.ts +114 -0
- package/lib/hook/diff.js +327 -0
- package/lib/hook/exportation.d.ts +31 -0
- package/lib/hook/exportation.js +24 -0
- package/lib/hook/file.d.ts +44 -0
- package/lib/hook/file.js +154 -0
- package/lib/hook/importation.d.ts +33 -0
- package/lib/hook/importation.js +22 -0
- package/lib/hook/index.d.ts +30 -0
- package/lib/hook/index.js +23 -0
- package/lib/hook/outline.d.ts +35 -0
- package/lib/hook/outline.js +27 -0
- package/lib/hook/project.d.ts +65 -0
- package/lib/hook/project.js +217 -0
- package/lib/hook/types.d.ts +114 -0
- package/lib/hook/types.js +407 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +9 -0
- package/lib/lock/lease.js +28 -3
- package/lib/manifest/cardSign.d.ts +1 -1
- package/lib/manifest/cardSign.js +37 -7
- package/lib/manifest/federation.js +13 -6
- package/lib/manifest/trust.d.ts +2 -0
- package/lib/manifest/trust.js +27 -6
- package/lib/module/index.d.ts +4 -0
- package/lib/module/index.js +4 -0
- package/lib/module/mcp/client.js +21 -2
- package/lib/module/mcp/filesystemServer.d.ts +24 -0
- package/lib/module/mcp/filesystemServer.js +102 -0
- package/lib/module/mcp/http.js +32 -19
- package/lib/module/mcp/workspaceServer.js +20 -3
- package/lib/module/tool/filesystemExecutor.d.ts +18 -0
- package/lib/module/tool/filesystemExecutor.js +49 -0
- package/lib/module/tool/globalExecutor.d.ts +17 -0
- package/lib/module/tool/globalExecutor.js +39 -0
- package/lib/module/tool/globalFilesystemExecutor.d.ts +18 -0
- package/lib/module/tool/globalFilesystemExecutor.js +49 -0
- package/lib/module/tool/workspaceExecutor.d.ts +9 -0
- package/lib/module/tool/workspaceExecutor.js +16 -3
- package/lib/naming/aliases.d.ts +48 -0
- package/lib/naming/aliases.js +79 -0
- package/lib/naming/catalog.d.ts +117 -0
- package/lib/naming/catalog.js +108 -0
- package/lib/naming/grammar.d.ts +67 -0
- package/lib/naming/grammar.js +212 -0
- package/lib/naming/ids.d.ts +157 -0
- package/lib/naming/ids.js +119 -0
- package/lib/naming/index.d.ts +13 -0
- package/lib/naming/index.js +13 -0
- package/lib/outline/index.d.ts +9 -0
- package/lib/outline/index.js +8 -0
- package/lib/outline/office.d.ts +160 -0
- package/lib/outline/office.js +2375 -0
- package/lib/outline/officeBridge.d.ts +32 -0
- package/lib/outline/officeBridge.js +27 -0
- package/lib/outline/outline.d.ts +159 -0
- package/lib/outline/outline.js +837 -0
- package/lib/outline/outlineIndex.d.ts +97 -0
- package/lib/outline/outlineIndex.js +325 -0
- package/lib/outline/outlineKeywords.d.ts +59 -0
- package/lib/outline/outlineKeywords.js +143 -0
- package/lib/outline/outlineStrategies.d.ts +414 -0
- package/lib/outline/outlineStrategies.js +8324 -0
- package/lib/outline/reactOutline.d.ts +37 -0
- package/lib/outline/reactOutline.js +1070 -0
- package/lib/outline/shellOutline.d.ts +65 -0
- package/lib/outline/shellOutline.js +873 -0
- package/lib/outline/sqlOutline.d.ts +170 -0
- package/lib/outline/sqlOutline.js +1536 -0
- package/lib/outline/strategy.d.ts +46 -0
- package/lib/outline/strategy.js +1 -0
- package/lib/outline/types.d.ts +95 -0
- package/lib/outline/types.js +1 -0
- package/lib/outline/visualBasicOutline.d.ts +27 -0
- package/lib/outline/visualBasicOutline.js +750 -0
- package/lib/protocol/filesystem.d.ts +55 -0
- package/lib/protocol/filesystem.js +105 -0
- package/lib/protocol/global.d.ts +42 -0
- package/lib/protocol/global.js +63 -0
- package/lib/protocol/globalFilesystem.d.ts +40 -0
- package/lib/protocol/globalFilesystem.js +59 -0
- package/lib/protocol/index.d.ts +4 -0
- package/lib/protocol/index.js +4 -0
- package/lib/protocol/protocol.d.ts +4 -0
- package/lib/protocol/wire.js +5 -2
- package/lib/protocol/workspace.d.ts +305 -0
- package/lib/protocol/workspace.js +258 -0
- package/lib/receipt/receipt.d.ts +8 -0
- package/lib/registry/content.js +9 -3
- package/lib/runtime/kernelRuntime.js +9 -1
- package/lib/scenario/scenario.js +5 -4
- package/lib/semantic/canonical.d.ts +61 -0
- package/lib/semantic/canonical.js +135 -8
- package/lib/semantic/jsonValue.d.ts +31 -0
- package/lib/semantic/jsonValue.js +122 -0
- package/lib/semantic/sha256.js +3 -0
- package/lib/semantic/time.d.ts +14 -0
- package/lib/semantic/time.js +56 -0
- package/lib/session/context.d.ts +76 -0
- package/lib/session/context.js +419 -0
- package/lib/session/errors.d.ts +24 -0
- package/lib/session/errors.js +22 -0
- package/lib/session/identity.d.ts +24 -0
- package/lib/session/identity.js +56 -0
- package/lib/session/index.d.ts +14 -0
- package/lib/session/index.js +12 -0
- package/lib/session/registry.d.ts +130 -0
- package/lib/session/registry.js +908 -0
- package/lib/session/types.d.ts +227 -0
- package/lib/session/types.js +14 -0
- package/lib/structure/index.d.ts +1 -0
- package/lib/structure/index.js +1 -0
- package/lib/structure/trie.d.ts +88 -0
- package/lib/structure/trie.js +183 -0
- package/lib/syntax/byteIndex.d.ts +44 -0
- package/lib/syntax/byteIndex.js +148 -0
- package/lib/syntax/charset.d.ts +88 -0
- package/lib/syntax/charset.js +276 -0
- package/lib/syntax/index.d.ts +6 -0
- package/lib/syntax/index.js +5 -0
- package/lib/syntax/syntaxRegistry.d.ts +150 -0
- package/lib/syntax/syntaxRegistry.js +314 -0
- package/lib/syntax/syntaxTree.d.ts +106 -0
- package/lib/syntax/syntaxTree.js +668 -0
- package/lib/syntax/types.d.ts +15 -0
- package/lib/syntax/types.js +1 -0
- package/lib/types/common.d.ts +1 -1
- package/package.json +72 -193
- package/.editorconfig +0 -12
- package/.prettierignore +0 -17
- package/MCP-Token/347/250/216/344/270/216/344/270/213/344/270/200/344/273/243/345/267/245/345/205/267/345/215/217/350/256/256.html +0 -1763
- package/docs/annotation-campaign-A.csv +0 -54
- package/docs/annotation-campaign-B.csv +0 -54
- package/docs/annotation-campaign.csv +0 -54
- package/docs/eval/annotations-A.jsonl +0 -16
- package/docs/eval/annotations.json +0 -122
- package/docs/eval/recorded-labels.json +0 -12
- package/docs/eval/replay-cases.json +0 -14
- package/docs/eval/replay-labels-b.json +0 -14
- package/docs/examples/attention-request.json +0 -16
- package/docs/examples/checkpoint.json +0 -15
- package/docs/examples/claim.json +0 -66
- package/docs/examples/collaboration.json +0 -66
- package/docs/examples/concept-checkpoint.json +0 -15
- package/docs/examples/concept-prune-plan.json +0 -45
- package/docs/examples/federation.json +0 -126
- package/docs/examples/frame-chain.json +0 -77
- package/docs/examples/ledger-snapshot.json +0 -108
- package/docs/examples/manifest.json +0 -77
- package/docs/examples/message.json +0 -19
- package/docs/examples/other-model.json +0 -45
- package/docs/examples/plan.json +0 -50
- package/docs/examples/prune-plan.json +0 -48
- package/docs/examples/receipt.json +0 -22
- package/docs/examples/runtime-run.json +0 -573
- package/docs/examples/team.json +0 -66
- package/docs/examples/verb-note.json +0 -11
- package/docs/probes/bayes-scores.json +0 -1452
- package/docs/probes/calibration-imported.json +0 -141
- package/docs/probes/context-ast-deepseek.json +0 -1577
- package/docs/probes/deepseek-probe.json +0 -917
- package/docs/probes/eval-recorded-replay.json +0 -597
- package/docs/probes/eval-replay.json +0 -124
- package/docs/probes/eval-summary.json +0 -42
- package/docs/probes/runtime-eval.json +0 -4230
- package/docs/schemas/collaboration.schema.json +0 -73
- package/docs/schemas/common.schema.json +0 -125
- package/docs/schemas/compaction.schema.json +0 -121
- package/docs/schemas/concept-compaction.schema.json +0 -73
- package/docs/schemas/concept.schema.json +0 -97
- package/docs/schemas/epistemic.schema.json +0 -178
- package/docs/schemas/federation.schema.json +0 -66
- package/docs/schemas/frame.schema.json +0 -86
- package/docs/schemas/ledger-snapshot.schema.json +0 -30
- package/docs/schemas/manifest.schema.json +0 -169
- package/docs/schemas/message.schema.json +0 -41
- package/docs/schemas/plan.schema.json +0 -189
- package/docs/schemas/receipt.schema.json +0 -45
- package/docs/schemas/social.schema.json +0 -153
- package/docs/schemas/team.schema.json +0 -56
- package/docs/schemas/verb.schema.json +0 -120
- package/eslint.config.mts +0 -96
- package/index.html +0 -13
- package/lib/context/workspaceProtocol.d.ts +0 -30
- package/lib/context/workspaceProtocol.js +0 -49
- package/prettier.config.mts +0 -37
- package/public/favicon.svg +0 -1
- package/public/icons.svg +0 -24
- package/src/attention/attention.ts +0 -347
- package/src/attention/budget.ts +0 -72
- package/src/attention/cognition.ts +0 -519
- package/src/attention/index.ts +0 -8
- package/src/attention/interrupts.ts +0 -228
- package/src/attention/resource.ts +0 -316
- package/src/attention/social.ts +0 -217
- package/src/attention/socialTypes.ts +0 -115
- package/src/authz/authz.ts +0 -305
- package/src/authz/index.ts +0 -1
- package/src/bayes/calibration.ts +0 -204
- package/src/bayes/calibrationRegistry.ts +0 -111
- package/src/bayes/calibrationSelection.ts +0 -154
- package/src/bayes/engine.ts +0 -532
- package/src/bayes/index.ts +0 -6
- package/src/bayes/reliability.ts +0 -62
- package/src/bayes/scopedPriors.ts +0 -88
- package/src/boundary/frame.ts +0 -322
- package/src/boundary/ids.ts +0 -14
- package/src/boundary/index.ts +0 -12
- package/src/boundary/refTable.ts +0 -125
- package/src/boundary/toolCall.ts +0 -212
- package/src/concept/concept.ts +0 -75
- package/src/concept/conceptBudget.ts +0 -346
- package/src/concept/conceptBudgetTypes.ts +0 -79
- package/src/concept/index.ts +0 -3
- package/src/concurrency/idempotency.ts +0 -41
- package/src/concurrency/index.ts +0 -5
- package/src/concurrency/mutex.ts +0 -26
- package/src/concurrency/namedLocks.ts +0 -35
- package/src/concurrency/retry.ts +0 -50
- package/src/concurrency/worker.ts +0 -128
- package/src/context/agentContext.ts +0 -308
- package/src/context/ast.ts +0 -499
- package/src/context/contentStore.ts +0 -75
- package/src/context/context.ts +0 -65
- package/src/context/contextIndex.ts +0 -85
- package/src/context/document.ts +0 -137
- package/src/context/fileStorePath.ts +0 -14
- package/src/context/index.ts +0 -43
- package/src/context/lifecycle.ts +0 -221
- package/src/context/projections.ts +0 -151
- package/src/context/workspace.ts +0 -422
- package/src/context/workspaceProtocol.ts +0 -112
- package/src/context/workspaceService.ts +0 -423
- package/src/cooperation/board.ts +0 -104
- package/src/cooperation/collaboration.ts +0 -60
- package/src/cooperation/index.ts +0 -6
- package/src/cooperation/merge.ts +0 -190
- package/src/cooperation/metrics.ts +0 -57
- package/src/cooperation/session.ts +0 -131
- package/src/cooperation/unified.ts +0 -56
- package/src/counter.ts +0 -9
- package/src/database/adapter/adapters.ts +0 -35
- package/src/database/adapter/index.ts +0 -4
- package/src/database/adapter/runtimeSink.ts +0 -74
- package/src/database/adapter/sessionStore.ts +0 -32
- package/src/database/adapter/turnStore.ts +0 -23
- package/src/database/entity/agent.ts +0 -199
- package/src/database/entity/entity.ts +0 -1046
- package/src/database/entity/handle.ts +0 -79
- package/src/database/entity/index.ts +0 -32
- package/src/database/entity/message.ts +0 -74
- package/src/database/entity/meta.ts +0 -11
- package/src/database/entity/object.ts +0 -42
- package/src/database/entity/outbox.ts +0 -15
- package/src/database/entity/process.ts +0 -65
- package/src/database/entity/registry.ts +0 -155
- package/src/database/entity/runtimeApproval.ts +0 -30
- package/src/database/entity/runtimeSession.ts +0 -13
- package/src/database/entity/runtimeTurn.ts +0 -15
- package/src/database/entity/thread.ts +0 -85
- package/src/database/entity/window.ts +0 -85
- package/src/database/entity/worker.ts +0 -90
- package/src/database/index.ts +0 -6
- package/src/database/repository/addressSpaceRepository.ts +0 -72
- package/src/database/repository/agentRepository.ts +0 -460
- package/src/database/repository/base.ts +0 -41
- package/src/database/repository/executionRepository.ts +0 -477
- package/src/database/repository/handleRepository.ts +0 -64
- package/src/database/repository/index.ts +0 -20
- package/src/database/repository/messageRepository.ts +0 -52
- package/src/database/repository/objectRepository.ts +0 -69
- package/src/database/repository/outboxRepository.ts +0 -63
- package/src/database/repository/processRepository.ts +0 -58
- package/src/database/repository/registryRepository.ts +0 -119
- package/src/database/repository/repository.ts +0 -491
- package/src/database/repository/runtimeApprovalRepository.ts +0 -44
- package/src/database/repository/runtimeSessionRepository.ts +0 -27
- package/src/database/repository/runtimeTurnRepository.ts +0 -39
- package/src/database/repository/socialRepository.ts +0 -185
- package/src/database/repository/threadRepository.ts +0 -57
- package/src/database/repository/windowRepository.ts +0 -88
- package/src/database/repository/workerRepository.ts +0 -69
- package/src/database/service/database.ts +0 -90
- package/src/database/service/handleService.ts +0 -48
- package/src/database/service/index.ts +0 -16
- package/src/database/service/messageService.ts +0 -61
- package/src/database/service/objectService.ts +0 -59
- package/src/database/service/processService.ts +0 -61
- package/src/database/service/registryServices.ts +0 -88
- package/src/database/service/service.ts +0 -183
- package/src/database/service/stepService.ts +0 -53
- package/src/database/service/threadService.ts +0 -65
- package/src/database/service/windowService.ts +0 -30
- package/src/database/service/workerService.ts +0 -57
- package/src/database/sql/index.ts +0 -3
- package/src/database/sql/schema.ts +0 -418
- package/src/database/sql/sql.ts +0 -543
- package/src/database/store/database.ts +0 -210
- package/src/database/store/index.ts +0 -3
- package/src/database/store/store.ts +0 -266
- package/src/delegate/index.ts +0 -2
- package/src/delegate/team.ts +0 -268
- package/src/delegate/teamTypes.ts +0 -47
- package/src/diagnosis/checklist.ts +0 -74
- package/src/diagnosis/context.ts +0 -93
- package/src/diagnosis/frame.ts +0 -256
- package/src/diagnosis/index.ts +0 -5
- package/src/diagnosis/router.ts +0 -72
- package/src/diagnosis/types.ts +0 -89
- package/src/error/agentError.ts +0 -16
- package/src/error/ampError.ts +0 -20
- package/src/error/authzError.ts +0 -14
- package/src/error/boundaryRejection.ts +0 -20
- package/src/error/index.ts +0 -7
- package/src/error/kernelError.ts +0 -27
- package/src/error/mailboxError.ts +0 -9
- package/src/error/windowError.ts +0 -12
- package/src/evaluation/agreement.ts +0 -96
- package/src/evaluation/index.ts +0 -2
- package/src/evaluation/labels.ts +0 -241
- package/src/evidence/evidenceGraph.ts +0 -205
- package/src/evidence/index.ts +0 -4
- package/src/evidence/revalidation.ts +0 -191
- package/src/execution/exec.ts +0 -408
- package/src/execution/handles.ts +0 -65
- package/src/execution/index.ts +0 -3
- package/src/execution/pin.ts +0 -154
- package/src/frame/frame.ts +0 -70
- package/src/frame/index.ts +0 -1
- package/src/goal/goal.ts +0 -43
- package/src/goal/index.ts +0 -2
- package/src/goal/plan.ts +0 -196
- package/src/governance/attentionManifest.ts +0 -802
- package/src/governance/audit.ts +0 -362
- package/src/governance/budget.ts +0 -628
- package/src/governance/context.ts +0 -356
- package/src/governance/control.ts +0 -159
- package/src/governance/cooperation.ts +0 -479
- package/src/governance/credential.ts +0 -359
- package/src/governance/httpServer.ts +0 -99
- package/src/governance/index.ts +0 -32
- package/src/governance/legal.ts +0 -574
- package/src/governance/matrix.ts +0 -460
- package/src/governance/perspectives.ts +0 -406
- package/src/governance/signing.ts +0 -407
- package/src/governance/store.ts +0 -125
- package/src/governance/switching.ts +0 -459
- package/src/governance/toolPolicy.ts +0 -80
- package/src/governance/transport.ts +0 -204
- package/src/governance/types.ts +0 -380
- package/src/governance/workflow.ts +0 -743
- package/src/index.ts +0 -11
- package/src/interaction/index.ts +0 -1
- package/src/interaction/interaction.ts +0 -391
- package/src/ipc/index.ts +0 -4
- package/src/ipc/mailbox.ts +0 -132
- package/src/ipc/memorySocialStore.ts +0 -93
- package/src/ipc/social.ts +0 -93
- package/src/kernel/index.ts +0 -14
- package/src/kernel/scheduler.ts +0 -498
- package/src/ledger/checkpoint.ts +0 -304
- package/src/ledger/index.ts +0 -4
- package/src/ledger/ledger.ts +0 -31
- package/src/ledger/persistence.ts +0 -159
- package/src/ledger/share.ts +0 -80
- package/src/lock/index.ts +0 -2
- package/src/lock/lease.ts +0 -74
- package/src/main.ts +0 -199
- package/src/manifest/agentCard.ts +0 -72
- package/src/manifest/cardSign.ts +0 -87
- package/src/manifest/federation.ts +0 -237
- package/src/manifest/federationTypes.ts +0 -55
- package/src/manifest/index.ts +0 -8
- package/src/manifest/keys.ts +0 -643
- package/src/manifest/manifest.ts +0 -114
- package/src/manifest/sign.ts +0 -134
- package/src/manifest/trust.ts +0 -146
- package/src/mission/index.ts +0 -2
- package/src/mission/missionQueue.ts +0 -75
- package/src/module/index.ts +0 -9
- package/src/module/llm/deepseek.ts +0 -214
- package/src/module/llm/llm.ts +0 -84
- package/src/module/mcp/client.ts +0 -110
- package/src/module/mcp/http.ts +0 -102
- package/src/module/mcp/protocol.ts +0 -77
- package/src/module/mcp/server.ts +0 -148
- package/src/module/mcp/transport.ts +0 -30
- package/src/module/mcp/workspaceServer.ts +0 -111
- package/src/module/sandbox/sandbox.ts +0 -29
- package/src/module/tool/mcpExecutor.ts +0 -54
- package/src/module/tool/policy.ts +0 -52
- package/src/module/tool/redact.ts +0 -29
- package/src/module/tool/tool.ts +0 -53
- package/src/module/tool/workspaceExecutor.ts +0 -35
- package/src/policy/guard.ts +0 -240
- package/src/policy/index.ts +0 -2
- package/src/policy/policy.ts +0 -561
- package/src/process/agent.ts +0 -341
- package/src/process/agentService.ts +0 -236
- package/src/process/agentStore.ts +0 -378
- package/src/process/executionStore.ts +0 -151
- package/src/process/index.ts +0 -5
- package/src/process/process.ts +0 -240
- package/src/process/transitions.ts +0 -19
- package/src/progress/index.ts +0 -2
- package/src/progress/progress.ts +0 -85
- package/src/protocol/a2a.ts +0 -120
- package/src/protocol/channel.ts +0 -44
- package/src/protocol/envelope.ts +0 -655
- package/src/protocol/httpChannel.ts +0 -87
- package/src/protocol/index.ts +0 -6
- package/src/protocol/protocol.ts +0 -138
- package/src/protocol/wire.ts +0 -60
- package/src/receipt/index.ts +0 -1
- package/src/receipt/receipt.ts +0 -47
- package/src/registry/content.ts +0 -69
- package/src/registry/index.ts +0 -23
- package/src/registry/registry.ts +0 -599
- package/src/runtime/action.ts +0 -87
- package/src/runtime/behavior.ts +0 -330
- package/src/runtime/behaviorTuning.ts +0 -163
- package/src/runtime/cognitiveRuntime.ts +0 -703
- package/src/runtime/escalation.ts +0 -35
- package/src/runtime/governedTask.ts +0 -233
- package/src/runtime/index.ts +0 -15
- package/src/runtime/kernelRuntime.ts +0 -144
- package/src/runtime/operation.ts +0 -35
- package/src/runtime/replay.ts +0 -71
- package/src/runtime/routing.ts +0 -58
- package/src/runtime/safety.ts +0 -48
- package/src/runtime/session.ts +0 -341
- package/src/runtime/sessionStore.ts +0 -43
- package/src/runtime/telemetry.ts +0 -55
- package/src/runtime/turnStore.ts +0 -30
- package/src/scenario/index.ts +0 -1
- package/src/scenario/scenario.ts +0 -299
- package/src/semantic/canonical.ts +0 -24
- package/src/semantic/index.ts +0 -2
- package/src/semantic/sha256.ts +0 -82
- package/src/storage/appendLog.ts +0 -372
- package/src/storage/fileAppendSink.ts +0 -40
- package/src/storage/index.ts +0 -2
- package/src/store/bindCache.ts +0 -22
- package/src/store/cache.ts +0 -191
- package/src/store/context.ts +0 -31
- package/src/store/handleStore.ts +0 -31
- package/src/store/index.ts +0 -9
- package/src/store/messageStore.ts +0 -31
- package/src/store/objectStore.ts +0 -31
- package/src/store/processStore.ts +0 -31
- package/src/store/threadStore.ts +0 -31
- package/src/store/windowStore.ts +0 -31
- package/src/structure/abstractSyntaxTree.ts +0 -493
- package/src/structure/avlTree.ts +0 -408
- package/src/structure/binarySearchTree.ts +0 -352
- package/src/structure/binaryTree.ts +0 -525
- package/src/structure/bloomFilter.ts +0 -231
- package/src/structure/compare.ts +0 -110
- package/src/structure/cuckooFilter.ts +0 -337
- package/src/structure/deque.ts +0 -223
- package/src/structure/filterKey.ts +0 -36
- package/src/structure/hash.ts +0 -279
- package/src/structure/heap.ts +0 -268
- package/src/structure/index.ts +0 -20
- package/src/structure/iterator.ts +0 -22
- package/src/structure/lru.ts +0 -619
- package/src/structure/ordered.ts +0 -399
- package/src/structure/priority.ts +0 -915
- package/src/structure/queue.ts +0 -168
- package/src/structure/redBlackTree.ts +0 -680
- package/src/structure/segmentTree.ts +0 -248
- package/src/structure/stack.ts +0 -118
- package/src/structure/unordered.ts +0 -407
- package/src/style.css +0 -278
- package/src/thread/index.ts +0 -2
- package/src/thread/thread.ts +0 -176
- package/src/thread/transitions.ts +0 -17
- package/src/types/common.ts +0 -119
- package/src/types/index.ts +0 -23
- package/src/warrant/epistemic.ts +0 -154
- package/src/warrant/index.ts +0 -3
- package/src/warrant/inquiry.ts +0 -166
- package/src/warrant/warrant.ts +0 -179
- package/src/window/compaction.ts +0 -403
- package/src/window/compactionTypes.ts +0 -119
- package/src/window/index.ts +0 -7
- package/src/window/page.ts +0 -18
- package/src/window/window.ts +0 -244
- package/tools/_helpers.ts +0 -34
- package/tools/a2a-check.ts +0 -110
- package/tools/a2a-flow-check.ts +0 -78
- package/tools/agent-card-check.ts +0 -76
- package/tools/agent-check.ts +0 -236
- package/tools/agent-store-check.ts +0 -153
- package/tools/agreement-check.ts +0 -27
- package/tools/annotation-server-check.ts +0 -48
- package/tools/annotation-server.ts +0 -230
- package/tools/annotation-status.ts +0 -65
- package/tools/approval-access-check.ts +0 -66
- package/tools/arch-check.ts +0 -72
- package/tools/attention-core-check.ts +0 -48
- package/tools/attention-resource-check.ts +0 -169
- package/tools/audit-check.ts +0 -207
- package/tools/authority-check.ts +0 -124
- package/tools/authz-check.ts +0 -138
- package/tools/bayes-check.ts +0 -162
- package/tools/bayes-probe.ts +0 -154
- package/tools/behavior-tuning-check.ts +0 -53
- package/tools/board-check.ts +0 -87
- package/tools/boundary-check.ts +0 -199
- package/tools/bundle-trust-check.ts +0 -96
- package/tools/calibrate-imported.ts +0 -108
- package/tools/calibration-check.ts +0 -44
- package/tools/calibration-registry-check.ts +0 -40
- package/tools/calibration-sim.ts +0 -40
- package/tools/card-sign-check.ts +0 -71
- package/tools/card-trust-check.ts +0 -78
- package/tools/channel-check.ts +0 -86
- package/tools/checkpoint-check.ts +0 -620
- package/tools/cleanup-style-configs.ts +0 -29
- package/tools/cognition-check.ts +0 -648
- package/tools/cognitive-runtime-check.ts +0 -609
- package/tools/collaboration-demo.ts +0 -78
- package/tools/compaction-check.ts +0 -266
- package/tools/concept-check.ts +0 -260
- package/tools/concurrency-check.ts +0 -242
- package/tools/conformance-runtime.ts +0 -389
- package/tools/conformance.ts +0 -651
- package/tools/context-ast-check.ts +0 -90
- package/tools/context-ast-deepseek.ts +0 -242
- package/tools/context-graph-bench.ts +0 -50
- package/tools/context-graph-check.ts +0 -229
- package/tools/context-index-check.ts +0 -48
- package/tools/context-lifecycle-check.ts +0 -69
- package/tools/context-projection-check.ts +0 -73
- package/tools/context-scope-check.ts +0 -134
- package/tools/context-storage-check.ts +0 -113
- package/tools/cooperation-check.ts +0 -517
- package/tools/cooperation-http-check.ts +0 -125
- package/tools/deepseek-llm-check.ts +0 -147
- package/tools/deepseek-probe.ts +0 -494
- package/tools/diagnosis-check.ts +0 -109
- package/tools/diagnosis-router-check.ts +0 -50
- package/tools/document-context-check.ts +0 -237
- package/tools/envelope-check.ts +0 -659
- package/tools/eslint-rules/index.ts +0 -26
- package/tools/eslint-rules/rules/jsdoc-comment-format.ts +0 -105
- package/tools/eslint-rules/rules/no-function-keyword.ts +0 -60
- package/tools/eslint-rules/rules/no-multiline-parameters.ts +0 -71
- package/tools/eslint-rules/rules/no-multiline-type-parameters.ts +0 -57
- package/tools/eslint-rules/rules/no-undefined-comparison.ts +0 -115
- package/tools/eslint-rules/rules/prefer-template-for-string-conversion.ts +0 -97
- package/tools/eslint-rules/rules/require-typeof-parentheses.ts +0 -49
- package/tools/eslint-rules/rules/require-variable-type.ts +0 -36
- package/tools/eslint-rules/rules/variable-naming.ts +0 -175
- package/tools/eval-recorded-replay.ts +0 -215
- package/tools/eval-replay.ts +0 -139
- package/tools/eval-summary.ts +0 -136
- package/tools/evidence-revalidation-check.ts +0 -235
- package/tools/exec-check.ts +0 -217
- package/tools/federation-check.ts +0 -166
- package/tools/federation-demo.ts +0 -45
- package/tools/filter-check.ts +0 -243
- package/tools/governance-check.ts +0 -680
- package/tools/governance-store-check.ts +0 -131
- package/tools/governed-task-check.ts +0 -227
- package/tools/guard-check.ts +0 -122
- package/tools/http-channel-check.ts +0 -125
- package/tools/import-campaign.ts +0 -28
- package/tools/inquiry-check.ts +0 -126
- package/tools/interaction-check.ts +0 -171
- package/tools/interrupts-check.ts +0 -173
- package/tools/ipc-check.ts +0 -170
- package/tools/kernel-async-check.ts +0 -136
- package/tools/kernel-check.ts +0 -369
- package/tools/kernel-runtime-check.ts +0 -276
- package/tools/keys-check.ts +0 -559
- package/tools/label-audit.ts +0 -70
- package/tools/labels-workflow-check.ts +0 -49
- package/tools/linear-structure-check.ts +0 -524
- package/tools/live-exec.ts +0 -62
- package/tools/live-governance-eval.ts +0 -813
- package/tools/live-legacy.ts +0 -50
- package/tools/lru-check.ts +0 -182
- package/tools/make-annotation-campaign.ts +0 -57
- package/tools/mcp-executor-check.ts +0 -33
- package/tools/merge-annotations.ts +0 -32
- package/tools/merge-check.ts +0 -98
- package/tools/metrics-check.ts +0 -84
- package/tools/net-probe.ts +0 -107
- package/tools/oauth-probe.ts +0 -117
- package/tools/outbox-check.ts +0 -50
- package/tools/persistence-check.ts +0 -144
- package/tools/pin-check.ts +0 -116
- package/tools/platform-check.ts +0 -281
- package/tools/policy-check.ts +0 -514
- package/tools/prettier-plugin/index.mts +0 -84
- package/tools/priority-check.ts +0 -627
- package/tools/probe-context7.ts +0 -63
- package/tools/registry-check.ts +0 -252
- package/tools/reliability-check.ts +0 -25
- package/tools/retry-check.ts +0 -85
- package/tools/rewrite-declaration-extensions.ts +0 -98
- package/tools/runtime-check.ts +0 -165
- package/tools/runtime-deepseek-check.ts +0 -85
- package/tools/runtime-eval.ts +0 -358
- package/tools/scenario-check.ts +0 -161
- package/tools/scenario-incident.ts +0 -162
- package/tools/scoped-priors-check.ts +0 -35
- package/tools/scoped-priors-pooling.ts +0 -34
- package/tools/segment-ast-check.ts +0 -649
- package/tools/session-check.ts +0 -196
- package/tools/session-store-check.ts +0 -73
- package/tools/share-check.ts +0 -88
- package/tools/sign-check.ts +0 -95
- package/tools/signing-check.ts +0 -275
- package/tools/snapshot-demo.ts +0 -65
- package/tools/social-check.ts +0 -146
- package/tools/structure-compare-check.ts +0 -95
- package/tools/support/documentScanner.ts +0 -577
- package/tools/support/texScanner.ts +0 -631
- package/tools/support/workspaceScanner.ts +0 -476
- package/tools/team-check.ts +0 -163
- package/tools/team-demo.ts +0 -36
- package/tools/telemetry-check.ts +0 -44
- package/tools/tex-context-check.ts +0 -263
- package/tools/tool-gate-check.ts +0 -98
- package/tools/tree-structure-check.ts +0 -786
- package/tools/trust-flow-check.ts +0 -158
- package/tools/turn-store-check.ts +0 -57
- package/tools/unified-check.ts +0 -83
- package/tools/validate.ts +0 -309
- package/tools/wire-check.ts +0 -96
- package/tools/workspace-agent-check.ts +0 -486
- package/tools/workspace-agent-demo.ts +0 -62
- package/tools/workspace-agent-server.ts +0 -101
- package/tools/workspace-context-check.ts +0 -331
- package/tools/workspace-context.ts +0 -131
- package/tsconfig.build.json +0 -14
- package/tsconfig.json +0 -24
- package/tsconfig.tools.json +0 -31
- package/viz/context-ast.json +0 -1577
|
@@ -0,0 +1,2375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Office document parsing.
|
|
3
|
+
*
|
|
4
|
+
* The agent workspace exposes `outline`, `import`, `export` and `dependency`
|
|
5
|
+
* for files, so an Office container must answer the same questions as a source
|
|
6
|
+
* file: what is inside, what does it point at, and what does it expose? This
|
|
7
|
+
* module does that without a third-party parser or a runtime dependency:
|
|
8
|
+
*
|
|
9
|
+
* - OOXML (`.docx`, `.xlsx`, `.pptx`) is a ZIP of XML parts. `zip.ts` reads the
|
|
10
|
+
* archive and a bounded tag scanner below reads the parts. The scanner is
|
|
11
|
+
* deliberately not a DOM: it records tag spans, attributes and matching close
|
|
12
|
+
* indices, which is enough to walk paragraphs, slides, sheets and formulas
|
|
13
|
+
* with a hard tag/depth limit.
|
|
14
|
+
* - Legacy binaries (`.doc`, `.xls`, `.ppt`, `.pub`) are OLE Compound Files.
|
|
15
|
+
* `cfb.ts` extracts named streams and this module recovers printable
|
|
16
|
+
* UTF-16LE/ASCII text. That recovery is heuristic and reported as such.
|
|
17
|
+
* - Access databases (`.accdb`, `.mdb`) use the Jet/ACE format rather than CFB.
|
|
18
|
+
* The header is recognised and catalog names are recovered on a best-effort
|
|
19
|
+
* basis; no page or column semantics are claimed.
|
|
20
|
+
* - Anything else is `kind: "unknown"` with an explicit warning instead of a
|
|
21
|
+
* guessed parse.
|
|
22
|
+
*
|
|
23
|
+
* Parsing is not semantic validation. Word headers/footnotes/comments,
|
|
24
|
+
* PowerPoint layouts/notes/charts, Excel formula results and Access table
|
|
25
|
+
* definitions are outside the model. Every result therefore carries warnings
|
|
26
|
+
* that name the unsupported surface; `officeImports`, `officeExports` and
|
|
27
|
+
* `officeReferences` expose the facts that were actually recovered.
|
|
28
|
+
*/
|
|
29
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
30
|
+
import { posix } from "node:path";
|
|
31
|
+
import { rawDigest } from "../fingerprint/raw.js";
|
|
32
|
+
import { readCfb } from "../file/cfb.js";
|
|
33
|
+
import { fail, isFilesystemError } from "../file/errors.js";
|
|
34
|
+
import { hasOfficeOutlineParser, registerOfficeOutlineParser } from "./officeBridge.js";
|
|
35
|
+
import { readZipEntries, zipText } from "../file/zip.js";
|
|
36
|
+
/**
|
|
37
|
+
* Largest Office file `parseOffice` will load. Legacy containers and OOXML
|
|
38
|
+
* packages can be much larger than a text outline; `parseOffice` still applies
|
|
39
|
+
* the same 64 MiB ceiling as the rest of the bounded readers.
|
|
40
|
+
*/
|
|
41
|
+
export const OfficeMaxDocumentBytes = 64 * 1024 * 1024;
|
|
42
|
+
/**
|
|
43
|
+
* Largest Office file the outline bridge accepts from raw bytes.
|
|
44
|
+
*
|
|
45
|
+
* `outline.ts` owns its own 4 MiB text ceiling and routes Office containers
|
|
46
|
+
* through `readOutlineBytes` before calling the registered parser. Keeping the
|
|
47
|
+
* same value here means a direct `officeOutlineParser` caller gets
|
|
48
|
+
* `quota_exceeded` before any container walk, never `binary_file`, and the two
|
|
49
|
+
* entry points agree on the bound.
|
|
50
|
+
*/
|
|
51
|
+
export const OfficeMaxOutlineBytes = 4 * 1024 * 1024;
|
|
52
|
+
const MaxSections = 2000;
|
|
53
|
+
const MaxReferences = 2000;
|
|
54
|
+
const MaxImports = 500;
|
|
55
|
+
const MaxExports = 1000;
|
|
56
|
+
const MaxWarnings = 48;
|
|
57
|
+
const MaxLabelLength = 200;
|
|
58
|
+
const MaxDetailLength = 200;
|
|
59
|
+
const MaxTextFragments = 500;
|
|
60
|
+
const MaxRawFragments = 4096;
|
|
61
|
+
const MaxTextScanBytes = 48 * 1024 * 1024;
|
|
62
|
+
const MaxXmlTags = 200000;
|
|
63
|
+
const MaxXmlDepth = 256;
|
|
64
|
+
const MaxRelationshipTargets = 500;
|
|
65
|
+
/**
|
|
66
|
+
* Extensions recognized as Office documents. The map is lowercase; `isOfficeFile`
|
|
67
|
+
* and `kindFromPath` both fold the caller path before lookup.
|
|
68
|
+
*/
|
|
69
|
+
const OfficeExtensions = {
|
|
70
|
+
".docx": "word",
|
|
71
|
+
".doc": "word",
|
|
72
|
+
".xlsx": "excel",
|
|
73
|
+
".xls": "excel",
|
|
74
|
+
".pptx": "powerpoint",
|
|
75
|
+
".ppt": "powerpoint",
|
|
76
|
+
".pub": "publisher",
|
|
77
|
+
".accdb": "access",
|
|
78
|
+
".mdb": "access",
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Fold a path to its lowercase extension. A dot-file or a path without an
|
|
82
|
+
* extension yields an empty string.
|
|
83
|
+
*
|
|
84
|
+
* @param path - Caller path.
|
|
85
|
+
* @returns Lowercase extension including the dot, or `""`.
|
|
86
|
+
*/
|
|
87
|
+
const extensionOf = (path) => {
|
|
88
|
+
const lower = path.toLowerCase();
|
|
89
|
+
const slash = lower.lastIndexOf("/");
|
|
90
|
+
const name = slash === -1 ? lower : lower.slice(slash + 1);
|
|
91
|
+
const dot = name.lastIndexOf(".");
|
|
92
|
+
return dot <= 0 ? "" : name.slice(dot);
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Classify a path by extension.
|
|
96
|
+
*
|
|
97
|
+
* @param path - Caller path.
|
|
98
|
+
* @returns Office family, or `unknown`.
|
|
99
|
+
*/
|
|
100
|
+
const kindFromPath = (path) => OfficeExtensions[extensionOf(path)] ?? "unknown";
|
|
101
|
+
/**
|
|
102
|
+
* Whether a path names a supported Office document. The check is purely
|
|
103
|
+
* syntactic: a file with a `.docx` suffix that is not a ZIP is still routed to
|
|
104
|
+
* the Office parser so it can report a structured warning.
|
|
105
|
+
*
|
|
106
|
+
* @param path - Root-relative or absolute path.
|
|
107
|
+
* @returns Whether the extension is one of the supported Office extensions.
|
|
108
|
+
*/
|
|
109
|
+
export const isOfficeFile = (path) => kindFromPath(path) !== "unknown";
|
|
110
|
+
/**
|
|
111
|
+
* Collapse whitespace and bound a label.
|
|
112
|
+
*
|
|
113
|
+
* @param value - Raw label.
|
|
114
|
+
* @param fallback - Text used when the label is empty.
|
|
115
|
+
* @returns A compact label.
|
|
116
|
+
*/
|
|
117
|
+
const cleanLabel = (value, fallback) => {
|
|
118
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
119
|
+
const chosen = text.length === 0 ? fallback : text;
|
|
120
|
+
return chosen.slice(0, MaxLabelLength);
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Collapse whitespace and bound a detail/target string.
|
|
124
|
+
*
|
|
125
|
+
* @param value - Raw value.
|
|
126
|
+
* @returns A compact value.
|
|
127
|
+
*/
|
|
128
|
+
const cleanDetail = (value) => value.replace(/\s+/g, " ").trim().slice(0, MaxDetailLength);
|
|
129
|
+
/**
|
|
130
|
+
* Read ASCII bytes from a buffer without assuming a zero terminator.
|
|
131
|
+
*
|
|
132
|
+
* @param bytes - Source bytes.
|
|
133
|
+
* @param offset - Start offset.
|
|
134
|
+
* @param length - Maximum byte count.
|
|
135
|
+
* @returns Printable ASCII characters only.
|
|
136
|
+
*/
|
|
137
|
+
const asciiAt = (bytes, offset, length) => {
|
|
138
|
+
const end = Math.min(bytes.length, offset + length);
|
|
139
|
+
let value = "";
|
|
140
|
+
for (let index = offset; index < end; index += 1) {
|
|
141
|
+
const code = bytes[index];
|
|
142
|
+
if (code < 0x20 || code > 0x7e) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
value += String.fromCharCode(code);
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Extract an errno code from an unknown thrown value without trusting it.
|
|
151
|
+
*
|
|
152
|
+
* @param error - Caught value.
|
|
153
|
+
* @returns The string code, or `""`.
|
|
154
|
+
*/
|
|
155
|
+
const errorCode = (error) => {
|
|
156
|
+
if (typeof (error) !== "object" || error === null) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
const code = error.code;
|
|
160
|
+
return typeof (code) === "string" ? code : "";
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Translate a host filesystem failure into the closed domain taxonomy.
|
|
164
|
+
*
|
|
165
|
+
* @param error - Caught value.
|
|
166
|
+
* @returns Never; throws a classified error.
|
|
167
|
+
*/
|
|
168
|
+
const mapFsError = (error) => {
|
|
169
|
+
if (isFilesystemError(error)) {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
switch (errorCode(error)) {
|
|
173
|
+
case "ENOENT": {
|
|
174
|
+
return fail("not_found");
|
|
175
|
+
}
|
|
176
|
+
case "EACCES":
|
|
177
|
+
case "EPERM": {
|
|
178
|
+
return fail("permission_denied");
|
|
179
|
+
}
|
|
180
|
+
case "EISDIR": {
|
|
181
|
+
return fail("not_file");
|
|
182
|
+
}
|
|
183
|
+
case "ENOTDIR": {
|
|
184
|
+
return fail("not_directory");
|
|
185
|
+
}
|
|
186
|
+
case "ELOOP": {
|
|
187
|
+
return fail("symlink_denied");
|
|
188
|
+
}
|
|
189
|
+
case "ENOSPC":
|
|
190
|
+
case "EDQUOT": {
|
|
191
|
+
return fail("quota_exceeded");
|
|
192
|
+
}
|
|
193
|
+
default: {
|
|
194
|
+
return fail("internal_error");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* `lstat` through the domain error taxonomy.
|
|
200
|
+
*
|
|
201
|
+
* @param absolute - Host absolute path.
|
|
202
|
+
* @returns File stats.
|
|
203
|
+
*/
|
|
204
|
+
const statOrFail = (absolute) => {
|
|
205
|
+
try {
|
|
206
|
+
return lstatSync(absolute);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return mapFsError(error);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Read whole-file bytes through the domain error taxonomy.
|
|
214
|
+
*
|
|
215
|
+
* @param absolute - Host absolute path.
|
|
216
|
+
* @returns File bytes.
|
|
217
|
+
*/
|
|
218
|
+
const readBytesOrFail = (absolute) => {
|
|
219
|
+
try {
|
|
220
|
+
return readFileSync(absolute);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return mapFsError(error);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Decode the five predefined XML entities and numeric character references.
|
|
228
|
+
* Unknown entities are left intact so a caller can see exactly what was not
|
|
229
|
+
* understood.
|
|
230
|
+
*
|
|
231
|
+
* @param value - Encoded text.
|
|
232
|
+
* @returns Decoded text.
|
|
233
|
+
*/
|
|
234
|
+
const decodeXmlEntities = (value) => value.replace(/&(#x[0-9A-Fa-f]+|#[0-9]+|[A-Za-z][A-Za-z0-9]*);/g, (match, entity) => {
|
|
235
|
+
if (entity === "amp") {
|
|
236
|
+
return "&";
|
|
237
|
+
}
|
|
238
|
+
if (entity === "lt") {
|
|
239
|
+
return "<";
|
|
240
|
+
}
|
|
241
|
+
if (entity === "gt") {
|
|
242
|
+
return ">";
|
|
243
|
+
}
|
|
244
|
+
if (entity === "quot") {
|
|
245
|
+
return '"';
|
|
246
|
+
}
|
|
247
|
+
if (entity === "apos") {
|
|
248
|
+
return "'";
|
|
249
|
+
}
|
|
250
|
+
if (entity === "nbsp") {
|
|
251
|
+
return "\u00a0";
|
|
252
|
+
}
|
|
253
|
+
if (entity.startsWith("#x") || entity.startsWith("#X")) {
|
|
254
|
+
const code = Number.parseInt(entity.slice(2), 16);
|
|
255
|
+
return Number.isSafeInteger(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : match;
|
|
256
|
+
}
|
|
257
|
+
if (entity.startsWith("#")) {
|
|
258
|
+
const code = Number.parseInt(entity.slice(1), 10);
|
|
259
|
+
return Number.isSafeInteger(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : match;
|
|
260
|
+
}
|
|
261
|
+
return match;
|
|
262
|
+
});
|
|
263
|
+
/**
|
|
264
|
+
* Remove comments, CDATA markers and any residual tags from a text fragment,
|
|
265
|
+
* then decode entities. Used for the content of `<w:t>`, `<a:t>`, `<f>` and
|
|
266
|
+
* similar leaf elements where nested markup is not expected.
|
|
267
|
+
*
|
|
268
|
+
* @param value - Leaf content.
|
|
269
|
+
* @returns Plain text.
|
|
270
|
+
*/
|
|
271
|
+
const xmlText = (value) => decodeXmlEntities(value
|
|
272
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
273
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
274
|
+
.replace(/<[^>]*>/g, ""));
|
|
275
|
+
/**
|
|
276
|
+
* Find the `>` that closes a tag, ignoring `>` characters inside quoted
|
|
277
|
+
* attribute values.
|
|
278
|
+
*
|
|
279
|
+
* @param xml - Source XML.
|
|
280
|
+
* @param start - Offset of `<`.
|
|
281
|
+
* @returns Offset of `>`, or `-1`.
|
|
282
|
+
*/
|
|
283
|
+
const findTagEnd = (xml, start) => {
|
|
284
|
+
let quote = "";
|
|
285
|
+
for (let index = start + 1; index < xml.length; index += 1) {
|
|
286
|
+
const char = xml[index];
|
|
287
|
+
if (quote.length > 0) {
|
|
288
|
+
if (char === quote) {
|
|
289
|
+
quote = "";
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (char === '"' || char === "'") {
|
|
294
|
+
quote = char;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (char === ">") {
|
|
298
|
+
return index;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return -1;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Parse the attribute section of an open tag. Attribute values may be single or
|
|
305
|
+
* double quoted; entity references are decoded.
|
|
306
|
+
*
|
|
307
|
+
* @param body - Tag text between `<` and `>`, trimmed.
|
|
308
|
+
* @param from - Index after the tag name.
|
|
309
|
+
* @returns Attribute map.
|
|
310
|
+
*/
|
|
311
|
+
const parseAttributes = (body, from) => {
|
|
312
|
+
const attrs = {};
|
|
313
|
+
let index = from;
|
|
314
|
+
while (index < body.length) {
|
|
315
|
+
while (index < body.length && /\s/.test(body[index])) {
|
|
316
|
+
index += 1;
|
|
317
|
+
}
|
|
318
|
+
if (index >= body.length || body[index] === "/") {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
const equals = body.indexOf("=", index);
|
|
322
|
+
if (equals === -1) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
const name = body.slice(index, equals).trim();
|
|
326
|
+
let valueStart = equals + 1;
|
|
327
|
+
while (valueStart < body.length && /\s/.test(body[valueStart])) {
|
|
328
|
+
valueStart += 1;
|
|
329
|
+
}
|
|
330
|
+
const quote = body[valueStart] ?? "";
|
|
331
|
+
if (quote !== '"' && quote !== "'") {
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
const valueEnd = body.indexOf(quote, valueStart + 1);
|
|
335
|
+
if (valueEnd === -1) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (name.length > 0) {
|
|
339
|
+
attrs[name] = decodeXmlEntities(body.slice(valueStart + 1, valueEnd));
|
|
340
|
+
}
|
|
341
|
+
index = valueEnd + 1;
|
|
342
|
+
}
|
|
343
|
+
return attrs;
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Scan XML into an ordered tag list with bounded nesting and tag count.
|
|
347
|
+
*
|
|
348
|
+
* This is intentionally not a validating parser. It recognises comments,
|
|
349
|
+
* processing instructions, CDATA sections and doctype declarations, handles
|
|
350
|
+
* quoted `>` characters and records matching close indices with a stack. A
|
|
351
|
+
* document that exceeds the tag or depth ceiling throws; Office callers turn
|
|
352
|
+
* that into a warning because a single oversized part should not discard the
|
|
353
|
+
* rest of the package.
|
|
354
|
+
*
|
|
355
|
+
* @param xml - XML text.
|
|
356
|
+
* @returns Tags with matching-close indices.
|
|
357
|
+
*/
|
|
358
|
+
const scanXml = (xml) => {
|
|
359
|
+
const tags = [];
|
|
360
|
+
const closeOf = [];
|
|
361
|
+
const stack = [];
|
|
362
|
+
let depth = 0;
|
|
363
|
+
let index = 0;
|
|
364
|
+
while (index < xml.length) {
|
|
365
|
+
const start = xml.indexOf("<", index);
|
|
366
|
+
if (start === -1) {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (xml.startsWith("<!--", start)) {
|
|
370
|
+
const end = xml.indexOf("-->", start + 4);
|
|
371
|
+
if (end === -1) {
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
index = end + 3;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (xml.startsWith("<![CDATA[", start)) {
|
|
378
|
+
const end = xml.indexOf("]]>", start + 9);
|
|
379
|
+
if (end === -1) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
index = end + 3;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const next = xml[start + 1] ?? "";
|
|
386
|
+
if (next === "!" || next === "?") {
|
|
387
|
+
const end = findTagEnd(xml, start);
|
|
388
|
+
if (end === -1) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
index = end + 1;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const end = findTagEnd(xml, start);
|
|
395
|
+
if (end === -1) {
|
|
396
|
+
throw new Error("unterminated xml tag");
|
|
397
|
+
}
|
|
398
|
+
const body = xml.slice(start + 1, end).trim();
|
|
399
|
+
index = end + 1;
|
|
400
|
+
if (body.length === 0) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const closing = body.startsWith("/");
|
|
404
|
+
const content = closing ? body.slice(1).trim() : body;
|
|
405
|
+
const selfClosing = !closing && content.endsWith("/");
|
|
406
|
+
const withoutSlash = selfClosing ? content.slice(0, -1).trimEnd() : content;
|
|
407
|
+
let nameEnd = 0;
|
|
408
|
+
while (nameEnd < withoutSlash.length && !/\s/.test(withoutSlash[nameEnd])) {
|
|
409
|
+
nameEnd += 1;
|
|
410
|
+
}
|
|
411
|
+
const name = withoutSlash.slice(0, nameEnd);
|
|
412
|
+
if (name.length === 0) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const colon = name.lastIndexOf(":");
|
|
416
|
+
const local = colon === -1 ? name : name.slice(colon + 1);
|
|
417
|
+
const attrs = closing ? {} : parseAttributes(withoutSlash, nameEnd);
|
|
418
|
+
const tagIndex = tags.length;
|
|
419
|
+
tags.push({ name, local, closing, selfClosing, attrs, start, end: end + 1 });
|
|
420
|
+
closeOf.push(-1);
|
|
421
|
+
if (tags.length > MaxXmlTags) {
|
|
422
|
+
throw new Error("xml tag limit exceeded");
|
|
423
|
+
}
|
|
424
|
+
if (closing) {
|
|
425
|
+
depth = Math.max(0, depth - 1);
|
|
426
|
+
const open = stack.pop();
|
|
427
|
+
if (open !== (void 0)) {
|
|
428
|
+
closeOf[open] = tagIndex;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else if (!selfClosing) {
|
|
432
|
+
depth += 1;
|
|
433
|
+
if (depth > MaxXmlDepth) {
|
|
434
|
+
throw new Error("xml nesting limit exceeded");
|
|
435
|
+
}
|
|
436
|
+
stack.push(tagIndex);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return { tags, closeOf, unclosed: stack.length };
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* Attribute lookup with namespace-prefix fallback: an exact `r:id` wins over a
|
|
443
|
+
* bare `id`, and `w:val` is found when the caller only knows the local name.
|
|
444
|
+
*
|
|
445
|
+
* @param tag - Tag to inspect.
|
|
446
|
+
* @param name - Qualified or local attribute name.
|
|
447
|
+
* @returns Attribute value, or `""`.
|
|
448
|
+
*/
|
|
449
|
+
const xmlAttr = (tag, name) => {
|
|
450
|
+
const direct = tag.attrs[name];
|
|
451
|
+
if (direct !== (void 0)) {
|
|
452
|
+
return direct;
|
|
453
|
+
}
|
|
454
|
+
const colon = name.lastIndexOf(":");
|
|
455
|
+
const local = colon === -1 ? name : name.slice(colon + 1);
|
|
456
|
+
const keys = Object.keys(tag.attrs);
|
|
457
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
458
|
+
const key = keys[index];
|
|
459
|
+
const keyColon = key.lastIndexOf(":");
|
|
460
|
+
const keyLocal = keyColon === -1 ? key : key.slice(keyColon + 1);
|
|
461
|
+
if (keyLocal === local) {
|
|
462
|
+
return tag.attrs[key];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return "";
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Text content of one element, using its precomputed matching close.
|
|
469
|
+
*
|
|
470
|
+
* @param xml - Source XML.
|
|
471
|
+
* @param scan - Tag scan for the same source.
|
|
472
|
+
* @param index - Index of the open tag.
|
|
473
|
+
* @returns Decoded inner text, or `""` when the element is self-closing or
|
|
474
|
+
* unclosed.
|
|
475
|
+
*/
|
|
476
|
+
const xmlInnerText = (xml, scan, index) => {
|
|
477
|
+
const tag = scan.tags[index];
|
|
478
|
+
if (tag.selfClosing) {
|
|
479
|
+
return "";
|
|
480
|
+
}
|
|
481
|
+
const close = scan.closeOf[index];
|
|
482
|
+
if (close < 0) {
|
|
483
|
+
return "";
|
|
484
|
+
}
|
|
485
|
+
return xmlText(xml.slice(tag.end, scan.tags[close].start));
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* First child element with a given local name inside an open element.
|
|
489
|
+
*
|
|
490
|
+
* @param scan - Tag scan.
|
|
491
|
+
* @param openIndex - Enclosing open tag.
|
|
492
|
+
* @param local - Local child name.
|
|
493
|
+
* @returns Index of the child, or `-1`.
|
|
494
|
+
*/
|
|
495
|
+
const xmlChild = (scan, openIndex, local) => {
|
|
496
|
+
const close = scan.closeOf[openIndex];
|
|
497
|
+
const end = close < 0 ? scan.tags.length : close;
|
|
498
|
+
for (let index = openIndex + 1; index < end; index += 1) {
|
|
499
|
+
const tag = scan.tags[index];
|
|
500
|
+
if (!tag.closing && tag.local === local) {
|
|
501
|
+
return index;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return -1;
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* Text of the first matching child element.
|
|
508
|
+
*
|
|
509
|
+
* @param xml - Source XML.
|
|
510
|
+
* @param scan - Tag scan.
|
|
511
|
+
* @param openIndex - Enclosing open tag.
|
|
512
|
+
* @param local - Local child name.
|
|
513
|
+
* @returns Decoded text, or `""`.
|
|
514
|
+
*/
|
|
515
|
+
const xmlChildText = (xml, scan, openIndex, local) => {
|
|
516
|
+
const child = xmlChild(scan, openIndex, local);
|
|
517
|
+
return child === -1 ? "" : xmlInnerText(xml, scan, child);
|
|
518
|
+
};
|
|
519
|
+
/**
|
|
520
|
+
* Resolve an internal OOXML relationship target against its source part.
|
|
521
|
+
*
|
|
522
|
+
* @param sourcePart - Part that owns the relationship.
|
|
523
|
+
* @param target - Relationship target as written.
|
|
524
|
+
* @returns Normalized package part name.
|
|
525
|
+
*/
|
|
526
|
+
const resolvePart = (sourcePart, target) => {
|
|
527
|
+
if (target.startsWith("/")) {
|
|
528
|
+
return posix.normalize(target.slice(1)).replace(/^\.\//, "");
|
|
529
|
+
}
|
|
530
|
+
return posix.normalize(posix.join(posix.dirname(sourcePart), target)).replace(/^\.\//, "");
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* Path of the relationships part for an OOXML part.
|
|
534
|
+
*
|
|
535
|
+
* @param part - Source part name.
|
|
536
|
+
* @returns Relationships part name.
|
|
537
|
+
*/
|
|
538
|
+
const relationshipsPart = (part) => {
|
|
539
|
+
const directory = posix.dirname(part);
|
|
540
|
+
const file = `${posix.basename(part)}.rels`;
|
|
541
|
+
return directory === "." ? `_rels/${file}` : `${directory}/_rels/${file}`;
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* Read a relationships part. Missing parts are normal (a minimal fixture or a
|
|
545
|
+
* package without links), so this returns an empty list rather than warning.
|
|
546
|
+
*
|
|
547
|
+
* @param entries - OOXML package parts.
|
|
548
|
+
* @param part - Source part name.
|
|
549
|
+
* @returns Relationships in document order.
|
|
550
|
+
*/
|
|
551
|
+
const readRelationships = (entries, part) => {
|
|
552
|
+
const xml = zipText(entries, relationshipsPart(part));
|
|
553
|
+
if (xml === (void 0)) {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
const scan = scanXml(xml);
|
|
557
|
+
const relationships = [];
|
|
558
|
+
for (let index = 0; index < scan.tags.length && relationships.length < MaxRelationshipTargets; index += 1) {
|
|
559
|
+
const tag = scan.tags[index];
|
|
560
|
+
if (tag.closing || tag.local !== "Relationship") {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const id = xmlAttr(tag, "Id");
|
|
564
|
+
const type = xmlAttr(tag, "Type");
|
|
565
|
+
const rawTarget = xmlAttr(tag, "Target");
|
|
566
|
+
if (rawTarget.length === 0) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const external = xmlAttr(tag, "TargetMode") === "External";
|
|
570
|
+
relationships.push({
|
|
571
|
+
id,
|
|
572
|
+
type,
|
|
573
|
+
target: external ? rawTarget : resolvePart(part, rawTarget),
|
|
574
|
+
external,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return relationships;
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* Index a relationship list by id.
|
|
581
|
+
*
|
|
582
|
+
* @param relationships - Relationships to index.
|
|
583
|
+
* @returns Id to relationship.
|
|
584
|
+
*/
|
|
585
|
+
const relationshipMap = (relationships) => {
|
|
586
|
+
const map = new Map();
|
|
587
|
+
for (let index = 0; index < relationships.length; index += 1) {
|
|
588
|
+
map.set(relationships[index].id, relationships[index]);
|
|
589
|
+
}
|
|
590
|
+
return map;
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* Create the empty document and its context.
|
|
594
|
+
*
|
|
595
|
+
* @param path - Root-relative path.
|
|
596
|
+
* @param kind - Initial family guess.
|
|
597
|
+
* @param format - Container format.
|
|
598
|
+
* @param bytes - Complete file bytes.
|
|
599
|
+
* @returns Context for one parse.
|
|
600
|
+
*/
|
|
601
|
+
const createContext = (path, kind, format, bytes) => {
|
|
602
|
+
const document = {
|
|
603
|
+
path,
|
|
604
|
+
kind,
|
|
605
|
+
format,
|
|
606
|
+
bytes: BigInt(bytes.byteLength),
|
|
607
|
+
fingerprint: rawDigest(bytes),
|
|
608
|
+
sections: [],
|
|
609
|
+
references: [],
|
|
610
|
+
imports: [],
|
|
611
|
+
exports: [],
|
|
612
|
+
metadata: {},
|
|
613
|
+
warnings: [],
|
|
614
|
+
};
|
|
615
|
+
return {
|
|
616
|
+
document,
|
|
617
|
+
sections: document.sections,
|
|
618
|
+
referenceKeys: new Set(),
|
|
619
|
+
exportKeys: new Set(),
|
|
620
|
+
importKeys: new Set(),
|
|
621
|
+
warningKeys: new Set(),
|
|
622
|
+
count: 0,
|
|
623
|
+
truncated: false,
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* Add a warning, deduplicated and bounded.
|
|
628
|
+
*
|
|
629
|
+
* @param context - Parse context.
|
|
630
|
+
* @param message - Human-readable warning.
|
|
631
|
+
*/
|
|
632
|
+
const pushWarning = (context, message) => {
|
|
633
|
+
const clean = message.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
634
|
+
if (clean.length === 0 || context.warningKeys.has(clean) || context.document.warnings.length >= MaxWarnings) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
context.warningKeys.add(clean);
|
|
638
|
+
context.document.warnings.push(clean);
|
|
639
|
+
};
|
|
640
|
+
/**
|
|
641
|
+
* Set one metadata value when it is non-empty.
|
|
642
|
+
*
|
|
643
|
+
* @param context - Parse context.
|
|
644
|
+
* @param key - Metadata key.
|
|
645
|
+
* @param value - Raw value.
|
|
646
|
+
*/
|
|
647
|
+
const setMetadata = (context, key, value) => {
|
|
648
|
+
const clean = value.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
649
|
+
if (clean.length > 0) {
|
|
650
|
+
context.document.metadata[key] = clean;
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* Append a section and return it, or `undefined` once the section budget is
|
|
655
|
+
* exhausted. Children are appended pre-order, so `index` matches document
|
|
656
|
+
* order.
|
|
657
|
+
*
|
|
658
|
+
* @param context - Parse context.
|
|
659
|
+
* @param parent - Parent section, or `undefined` for a root section.
|
|
660
|
+
* @param kind - Section kind.
|
|
661
|
+
* @param label - Raw label.
|
|
662
|
+
* @param depth - Hierarchy depth.
|
|
663
|
+
* @returns The new section, or `undefined` when truncated.
|
|
664
|
+
*/
|
|
665
|
+
const appendSection = (context, parent, kind, label, depth) => {
|
|
666
|
+
if (context.count >= MaxSections) {
|
|
667
|
+
context.truncated = true;
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
const index = context.count;
|
|
671
|
+
context.count += 1;
|
|
672
|
+
const section = {
|
|
673
|
+
id: `${kind}-${index}`,
|
|
674
|
+
label: cleanLabel(label, `Section ${index + 1}`),
|
|
675
|
+
kind,
|
|
676
|
+
depth: Math.max(0, Math.trunc(depth)),
|
|
677
|
+
index,
|
|
678
|
+
children: [],
|
|
679
|
+
};
|
|
680
|
+
if (parent === (void 0)) {
|
|
681
|
+
context.sections.push(section);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
parent.children.push(section);
|
|
685
|
+
}
|
|
686
|
+
return section;
|
|
687
|
+
};
|
|
688
|
+
/**
|
|
689
|
+
* Add an outbound reference, deduplicated by kind/from/target.
|
|
690
|
+
*
|
|
691
|
+
* @param context - Parse context.
|
|
692
|
+
* @param reference - Reference to add.
|
|
693
|
+
*/
|
|
694
|
+
const addReference = (context, reference) => {
|
|
695
|
+
if (context.document.references.length >= MaxReferences) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const target = cleanDetail(reference.target);
|
|
699
|
+
const from = cleanDetail(reference.from);
|
|
700
|
+
if (target.length === 0) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const key = `${reference.kind}\u0000${from}\u0000${target}`;
|
|
704
|
+
if (context.referenceKeys.has(key)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
context.referenceKeys.add(key);
|
|
708
|
+
const detail = cleanDetail(reference.detail ?? "");
|
|
709
|
+
context.document.references.push({
|
|
710
|
+
kind: reference.kind,
|
|
711
|
+
from,
|
|
712
|
+
target,
|
|
713
|
+
...(detail.length === 0 ? {} : { detail }),
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* Add an external import target, deduplicated.
|
|
718
|
+
*
|
|
719
|
+
* @param context - Parse context.
|
|
720
|
+
* @param target - URL or external path.
|
|
721
|
+
*/
|
|
722
|
+
const addImport = (context, target) => {
|
|
723
|
+
const clean = cleanDetail(target);
|
|
724
|
+
if (clean.length === 0 || context.importKeys.has(clean) || context.document.imports.length >= MaxImports) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
context.importKeys.add(clean);
|
|
728
|
+
context.document.imports.push(clean);
|
|
729
|
+
};
|
|
730
|
+
/**
|
|
731
|
+
* Add an exported name, deduplicated.
|
|
732
|
+
*
|
|
733
|
+
* @param context - Parse context.
|
|
734
|
+
* @param name - Exported symbol or object name.
|
|
735
|
+
*/
|
|
736
|
+
const addExport = (context, name) => {
|
|
737
|
+
const clean = cleanLabel(name, "");
|
|
738
|
+
if (clean.length === 0 || context.exportKeys.has(clean) || context.document.exports.length >= MaxExports) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
context.exportKeys.add(clean);
|
|
742
|
+
context.document.exports.push(clean);
|
|
743
|
+
};
|
|
744
|
+
/**
|
|
745
|
+
* Read `docProps/core.xml` and `docProps/app.xml` into metadata.
|
|
746
|
+
*
|
|
747
|
+
* @param context - Parse context.
|
|
748
|
+
* @param entries - OOXML package parts.
|
|
749
|
+
*/
|
|
750
|
+
const readDocumentProperties = (context, entries) => {
|
|
751
|
+
const core = zipText(entries, "docProps/core.xml");
|
|
752
|
+
if (core !== (void 0)) {
|
|
753
|
+
const scan = scanXml(core);
|
|
754
|
+
const fieldKeys = {
|
|
755
|
+
title: "title",
|
|
756
|
+
creator: "creator",
|
|
757
|
+
lastModifiedBy: "lastModifiedBy",
|
|
758
|
+
created: "created",
|
|
759
|
+
modified: "modified",
|
|
760
|
+
subject: "subject",
|
|
761
|
+
description: "description",
|
|
762
|
+
keywords: "keywords",
|
|
763
|
+
category: "category",
|
|
764
|
+
};
|
|
765
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
766
|
+
const tag = scan.tags[index];
|
|
767
|
+
if (tag.closing || tag.selfClosing) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const key = fieldKeys[tag.local];
|
|
771
|
+
if (key === (void 0)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
setMetadata(context, key, xmlInnerText(core, scan, index));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const app = zipText(entries, "docProps/app.xml");
|
|
778
|
+
if (app !== (void 0)) {
|
|
779
|
+
const scan = scanXml(app);
|
|
780
|
+
const fieldKeys = {
|
|
781
|
+
Application: "application",
|
|
782
|
+
Company: "company",
|
|
783
|
+
AppVersion: "appVersion",
|
|
784
|
+
Pages: "pages",
|
|
785
|
+
Words: "words",
|
|
786
|
+
Slides: "slides",
|
|
787
|
+
Worksheets: "worksheets",
|
|
788
|
+
};
|
|
789
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
790
|
+
const tag = scan.tags[index];
|
|
791
|
+
if (tag.closing || tag.selfClosing) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const key = fieldKeys[tag.local];
|
|
795
|
+
if (key === (void 0)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
setMetadata(context, key, xmlInnerText(app, scan, index));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* Heading level for a Word paragraph, from an explicit outline level (which
|
|
804
|
+
* wins) or a built-in Heading/Title style id.
|
|
805
|
+
*
|
|
806
|
+
* @param style - Style id from `<w:pStyle w:val>`.
|
|
807
|
+
* @param outline - Raw outline level from `<w:outlineLvl w:val>`.
|
|
808
|
+
* @returns One-based heading level, or `0` for body text.
|
|
809
|
+
*/
|
|
810
|
+
const wordHeadingLevel = (style, outline) => {
|
|
811
|
+
if (outline.length > 0) {
|
|
812
|
+
const value = Number.parseInt(outline, 10);
|
|
813
|
+
if (Number.isSafeInteger(value) && value >= 0 && value <= 8) {
|
|
814
|
+
return value + 1;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const heading = /^Heading\s*([1-9])$/i.exec(style.trim());
|
|
818
|
+
if (heading !== null) {
|
|
819
|
+
return Number(heading[1]);
|
|
820
|
+
}
|
|
821
|
+
return /^Title$/i.test(style.trim()) ? 1 : 0;
|
|
822
|
+
};
|
|
823
|
+
/**
|
|
824
|
+
* Parse `word/document.xml` into heading/paragraph sections plus bookmarks.
|
|
825
|
+
*
|
|
826
|
+
* @param context - Parse context.
|
|
827
|
+
* @param entries - OOXML package parts.
|
|
828
|
+
*/
|
|
829
|
+
const parseWord = (context, entries) => {
|
|
830
|
+
const xml = zipText(entries, "word/document.xml");
|
|
831
|
+
if (xml === (void 0)) {
|
|
832
|
+
pushWarning(context, "word package has no word/document.xml");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const scan = scanXml(xml);
|
|
836
|
+
const paragraphs = [];
|
|
837
|
+
let current;
|
|
838
|
+
let nested = 0;
|
|
839
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
840
|
+
const tag = scan.tags[index];
|
|
841
|
+
if (tag.local === "p" && !tag.closing) {
|
|
842
|
+
if (tag.selfClosing) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (current === (void 0)) {
|
|
846
|
+
current = { texts: [], style: "", outline: "" };
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
nested += 1;
|
|
850
|
+
}
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (tag.local === "p" && tag.closing) {
|
|
854
|
+
if (current === (void 0)) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (nested > 0) {
|
|
858
|
+
nested -= 1;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
paragraphs.push({ text: current.texts.join(""), style: current.style, outline: current.outline });
|
|
862
|
+
current = undefined;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (tag.local === "bookmarkStart" && !tag.closing) {
|
|
866
|
+
const bookmark = xmlAttr(tag, "w:name");
|
|
867
|
+
if (bookmark.length > 0 && bookmark !== "_GoBack") {
|
|
868
|
+
addExport(context, bookmark);
|
|
869
|
+
addReference(context, { kind: "bookmark", from: context.document.path, target: bookmark, detail: "bookmark" });
|
|
870
|
+
}
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (current === (void 0)) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (tag.local === "t" && !tag.closing) {
|
|
877
|
+
const text = xmlInnerText(xml, scan, index);
|
|
878
|
+
if (text.length > 0) {
|
|
879
|
+
current.texts.push(text);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
else if (tag.local === "pStyle" && !tag.closing) {
|
|
883
|
+
current.style = xmlAttr(tag, "w:val");
|
|
884
|
+
}
|
|
885
|
+
else if (tag.local === "outlineLvl" && !tag.closing) {
|
|
886
|
+
current.outline = xmlAttr(tag, "w:val");
|
|
887
|
+
}
|
|
888
|
+
else if (tag.local === "hyperlink" && !tag.closing) {
|
|
889
|
+
const anchor = xmlAttr(tag, "w:anchor");
|
|
890
|
+
if (anchor.length > 0) {
|
|
891
|
+
addReference(context, { kind: "bookmark", from: context.document.path, target: `#${anchor}`, detail: "internal bookmark" });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (current !== (void 0)) {
|
|
896
|
+
paragraphs.push({ text: current.texts.join(""), style: current.style, outline: current.outline });
|
|
897
|
+
}
|
|
898
|
+
const headingStack = [];
|
|
899
|
+
for (let index = 0; index < paragraphs.length; index += 1) {
|
|
900
|
+
const paragraph = paragraphs[index];
|
|
901
|
+
const text = cleanLabel(paragraph.text, "");
|
|
902
|
+
if (text.length === 0) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const level = wordHeadingLevel(paragraph.style, paragraph.outline);
|
|
906
|
+
if (level > 0) {
|
|
907
|
+
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
|
|
908
|
+
headingStack.pop();
|
|
909
|
+
}
|
|
910
|
+
const parent = headingStack[headingStack.length - 1];
|
|
911
|
+
const section = appendSection(context, parent === (void 0) ? undefined : parent.section, "heading", text, parent === (void 0) ? level - 1 : parent.section.depth + 1);
|
|
912
|
+
if (section !== (void 0)) {
|
|
913
|
+
headingStack.push({ level, section });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
const parent = headingStack[headingStack.length - 1];
|
|
918
|
+
appendSection(context, parent === (void 0) ? undefined : parent.section, "paragraph", text, parent === (void 0) ? 0 : parent.section.depth + 1);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const relationships = readRelationships(entries, "word/document.xml");
|
|
922
|
+
for (let index = 0; index < relationships.length; index += 1) {
|
|
923
|
+
const relationship = relationships[index];
|
|
924
|
+
const type = relationship.type.toLowerCase();
|
|
925
|
+
if (type.endsWith("/hyperlink")) {
|
|
926
|
+
addReference(context, {
|
|
927
|
+
kind: "hyperlink",
|
|
928
|
+
from: context.document.path,
|
|
929
|
+
target: relationship.target,
|
|
930
|
+
detail: relationship.external ? "external hyperlink" : "internal hyperlink",
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
else if (relationship.external) {
|
|
934
|
+
addReference(context, { kind: "external-link", from: context.document.path, target: relationship.target, detail: "external relationship" });
|
|
935
|
+
}
|
|
936
|
+
else if (type.endsWith("/image") ||
|
|
937
|
+
type.endsWith("/oleobject") ||
|
|
938
|
+
type.endsWith("/package") ||
|
|
939
|
+
type.endsWith("/diagramdata") ||
|
|
940
|
+
type.endsWith("/chart")) {
|
|
941
|
+
addReference(context, { kind: "embedded", from: context.document.path, target: relationship.target, detail: "embedded part" });
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (relationship.external) {
|
|
947
|
+
addImport(context, relationship.target);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
pushWarning(context, "word: main document body only; headers, footnotes, comments, revisions, text boxes and embedded objects are not expanded");
|
|
951
|
+
};
|
|
952
|
+
/**
|
|
953
|
+
* Read shared strings. The cell content is not projected into the outline, but
|
|
954
|
+
* reading the part validates it and records its size; a malformed shared-string
|
|
955
|
+
* part is downgraded to a warning rather than failing the workbook.
|
|
956
|
+
*
|
|
957
|
+
* @param context - Parse context.
|
|
958
|
+
* @param entries - OOXML package parts.
|
|
959
|
+
* @returns Decoded shared strings.
|
|
960
|
+
*/
|
|
961
|
+
const readSharedStrings = (context, entries) => {
|
|
962
|
+
const xml = zipText(entries, "xl/sharedStrings.xml");
|
|
963
|
+
if (xml === (void 0)) {
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
const scan = scanXml(xml);
|
|
968
|
+
const strings = [];
|
|
969
|
+
for (let index = 0; index < scan.tags.length && strings.length < 10000; index += 1) {
|
|
970
|
+
const tag = scan.tags[index];
|
|
971
|
+
if (tag.closing || tag.selfClosing || tag.local !== "si") {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const close = scan.closeOf[index];
|
|
975
|
+
const end = close < 0 ? scan.tags.length : close;
|
|
976
|
+
let text = "";
|
|
977
|
+
for (let child = index + 1; child < end; child += 1) {
|
|
978
|
+
const childTag = scan.tags[child];
|
|
979
|
+
if (!childTag.closing && childTag.local === "t") {
|
|
980
|
+
text += xmlInnerText(xml, scan, child);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
strings.push(text);
|
|
984
|
+
}
|
|
985
|
+
return strings;
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
pushWarning(context, `excel: shared strings ignored (${error instanceof Error ? error.message : "parse failure"})`);
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
/**
|
|
993
|
+
* Read the workbook sheet list and defined names. Sheet order is the order of
|
|
994
|
+
* `<sheet>` elements; each is resolved through the workbook relationships with
|
|
995
|
+
* a name-based fallback for minimal packages.
|
|
996
|
+
*
|
|
997
|
+
* @param context - Parse context.
|
|
998
|
+
* @param entries - OOXML package parts.
|
|
999
|
+
* @returns Sheets and defined names.
|
|
1000
|
+
*/
|
|
1001
|
+
const readWorkbook = (context, entries) => {
|
|
1002
|
+
const xml = zipText(entries, "xl/workbook.xml");
|
|
1003
|
+
if (xml === (void 0)) {
|
|
1004
|
+
pushWarning(context, "excel package has no xl/workbook.xml");
|
|
1005
|
+
return { sheets: [], names: [] };
|
|
1006
|
+
}
|
|
1007
|
+
const scan = scanXml(xml);
|
|
1008
|
+
const relationships = relationshipMap(readRelationships(entries, "xl/workbook.xml"));
|
|
1009
|
+
const sheets = [];
|
|
1010
|
+
const names = [];
|
|
1011
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
1012
|
+
const tag = scan.tags[index];
|
|
1013
|
+
if (tag.closing) {
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (tag.local === "sheet") {
|
|
1017
|
+
const name = cleanLabel(xmlAttr(tag, "name"), `Sheet ${sheets.length + 1}`);
|
|
1018
|
+
const relationId = xmlAttr(tag, "r:id");
|
|
1019
|
+
const relationship = relationships.get(relationId);
|
|
1020
|
+
const fallback = `xl/worksheets/sheet${sheets.length + 1}.xml`;
|
|
1021
|
+
sheets.push({ name, relationId, part: relationship === (void 0) ? fallback : relationship.target });
|
|
1022
|
+
}
|
|
1023
|
+
else if (tag.local === "definedName") {
|
|
1024
|
+
const name = cleanLabel(xmlAttr(tag, "name"), "");
|
|
1025
|
+
if (name.length === 0) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const localRaw = xmlAttr(tag, "localSheetId");
|
|
1029
|
+
const localSheetIndex = localRaw.length === 0 ? -1 : Number.parseInt(localRaw, 10);
|
|
1030
|
+
names.push({ name, localSheetIndex: Number.isSafeInteger(localSheetIndex) ? localSheetIndex : -1, formula: xmlInnerText(xml, scan, index) });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return { sheets, names };
|
|
1034
|
+
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Replace double-quoted Excel string literals with spaces, preserving offsets
|
|
1037
|
+
* so reference positions in the original formula remain meaningful.
|
|
1038
|
+
*
|
|
1039
|
+
* @param formula - Formula text.
|
|
1040
|
+
* @returns Masked character array joined back into a string.
|
|
1041
|
+
*/
|
|
1042
|
+
const maskFormulaStrings = (formula) => {
|
|
1043
|
+
const chars = formula.split("");
|
|
1044
|
+
let index = 0;
|
|
1045
|
+
while (index < chars.length) {
|
|
1046
|
+
if (chars[index] !== '"') {
|
|
1047
|
+
index += 1;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
chars[index] = " ";
|
|
1051
|
+
index += 1;
|
|
1052
|
+
while (index < chars.length) {
|
|
1053
|
+
if (chars[index] === '"') {
|
|
1054
|
+
if (index + 1 < chars.length && chars[index + 1] === '"') {
|
|
1055
|
+
chars[index] = " ";
|
|
1056
|
+
chars[index + 1] = " ";
|
|
1057
|
+
index += 2;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
chars[index] = " ";
|
|
1061
|
+
index += 1;
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
chars[index] = " ";
|
|
1065
|
+
index += 1;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return chars.join("");
|
|
1069
|
+
};
|
|
1070
|
+
/**
|
|
1071
|
+
* Blank a matched span so a later, less specific regex cannot report the same
|
|
1072
|
+
* token as a different reference kind.
|
|
1073
|
+
*
|
|
1074
|
+
* @param chars - Mutable character array.
|
|
1075
|
+
* @param start - Match start.
|
|
1076
|
+
* @param length - Match length.
|
|
1077
|
+
*/
|
|
1078
|
+
const maskSpan = (chars, start, length) => {
|
|
1079
|
+
for (let index = start; index < start + length && index < chars.length; index += 1) {
|
|
1080
|
+
chars[index] = " ";
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
/**
|
|
1084
|
+
* Extract A1, Sheet!A1 and [n]Sheet!A1 references from one formula.
|
|
1085
|
+
*
|
|
1086
|
+
* External workbook references are resolved through the package's external
|
|
1087
|
+
* link parts when available; the unresolved `[n]` form is kept otherwise. Only
|
|
1088
|
+
* lexical references are reported: formulas are not evaluated, shared formulas
|
|
1089
|
+
* are not expanded and dynamic references are not inferred.
|
|
1090
|
+
*
|
|
1091
|
+
* @param context - Parse context.
|
|
1092
|
+
* @param formula - Cell formula without the leading `=`.
|
|
1093
|
+
* @param sheetName - Sheet that owns the cell.
|
|
1094
|
+
* @param address - Cell address such as `B4`.
|
|
1095
|
+
* @param externalTargets - External link index to target path.
|
|
1096
|
+
*/
|
|
1097
|
+
const addFormulaReferences = (context, formula, sheetName, address, externalTargets) => {
|
|
1098
|
+
if (formula.length === 0) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const masked = maskFormulaStrings(formula);
|
|
1102
|
+
const chars = masked.split("");
|
|
1103
|
+
const from = `${sheetName}!${address}`;
|
|
1104
|
+
const external = /(?:'\[(\d+)\]([^']*)'|\[(\d+)\]([A-Za-z0-9_.]+))!(\$?[A-Z]{1,3}\$?[1-9]\d*)/g;
|
|
1105
|
+
let match = external.exec(masked);
|
|
1106
|
+
while (match !== null) {
|
|
1107
|
+
const linkIndex = Number(match[1] ?? match[3]);
|
|
1108
|
+
const targetSheet = match[2] ?? match[4] ?? "";
|
|
1109
|
+
const targetAddress = match[5];
|
|
1110
|
+
const target = `[${linkIndex}]${targetSheet}!${targetAddress}`;
|
|
1111
|
+
addReference(context, { kind: "formula", from, target, detail: "external workbook reference" });
|
|
1112
|
+
const resolved = externalTargets.get(linkIndex);
|
|
1113
|
+
if (resolved !== (void 0)) {
|
|
1114
|
+
addImport(context, resolved);
|
|
1115
|
+
}
|
|
1116
|
+
maskSpan(chars, match.index, match[0].length);
|
|
1117
|
+
match = external.exec(masked);
|
|
1118
|
+
}
|
|
1119
|
+
const afterExternal = chars.join("");
|
|
1120
|
+
const sheetPattern = /(?<![\w\]'])(?:'((?:[^']|'')+)'|([A-Za-z_][A-Za-z0-9_.]*))!(\$?[A-Z]{1,3}\$?[1-9]\d*)/g;
|
|
1121
|
+
let sheetMatch = sheetPattern.exec(afterExternal);
|
|
1122
|
+
while (sheetMatch !== null) {
|
|
1123
|
+
const targetSheet = (sheetMatch[1] ?? sheetMatch[2] ?? "").replace(/''/g, "'");
|
|
1124
|
+
const targetAddress = sheetMatch[3];
|
|
1125
|
+
if (!targetSheet.startsWith("[")) {
|
|
1126
|
+
addReference(context, { kind: "formula", from, target: `${targetSheet}!${targetAddress}`, detail: "sheet reference" });
|
|
1127
|
+
maskSpan(chars, sheetMatch.index, sheetMatch[0].length);
|
|
1128
|
+
}
|
|
1129
|
+
sheetMatch = sheetPattern.exec(afterExternal);
|
|
1130
|
+
}
|
|
1131
|
+
const rest = chars.join("");
|
|
1132
|
+
const plain = /(?<![A-Za-z0-9_.$])\$?[A-Z]{1,3}\$?[1-9]\d*(?![A-Za-z0-9_(])/g;
|
|
1133
|
+
let plainMatch = plain.exec(rest);
|
|
1134
|
+
while (plainMatch !== null) {
|
|
1135
|
+
addReference(context, { kind: "formula", from, target: plainMatch[0], detail: "cell reference" });
|
|
1136
|
+
plainMatch = plain.exec(rest);
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
/**
|
|
1140
|
+
* Read external-link targets. `xl/externalLinks/externalLinkN.xml` has a
|
|
1141
|
+
* relationships part whose external target is the linked workbook.
|
|
1142
|
+
*
|
|
1143
|
+
* @param entries - OOXML package parts.
|
|
1144
|
+
* @returns External link index to target.
|
|
1145
|
+
*/
|
|
1146
|
+
const readExternalTargets = (entries) => {
|
|
1147
|
+
const targets = new Map();
|
|
1148
|
+
const names = Array.from(entries.keys()).filter((name) => /^xl\/externalLinks\/externalLink[0-9]+\.xml$/.test(name));
|
|
1149
|
+
names.sort((left, right) => {
|
|
1150
|
+
const leftNumber = Number(/externalLink([0-9]+)\.xml$/.exec(left)?.[1] ?? 0);
|
|
1151
|
+
const rightNumber = Number(/externalLink([0-9]+)\.xml$/.exec(right)?.[1] ?? 0);
|
|
1152
|
+
return leftNumber - rightNumber;
|
|
1153
|
+
});
|
|
1154
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
1155
|
+
const name = names[index];
|
|
1156
|
+
const linkIndex = Number(/externalLink([0-9]+)\.xml$/.exec(name)?.[1] ?? names.length + 1);
|
|
1157
|
+
const relationships = readRelationships(entries, name);
|
|
1158
|
+
const external = relationships.find((relationship) => relationship.external);
|
|
1159
|
+
if (external !== (void 0)) {
|
|
1160
|
+
targets.set(linkIndex, external.target);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return targets;
|
|
1164
|
+
};
|
|
1165
|
+
/**
|
|
1166
|
+
* Read worksheet tables through the worksheet relationships.
|
|
1167
|
+
*
|
|
1168
|
+
* @param entries - OOXML package parts.
|
|
1169
|
+
* @param sheetPart - Worksheet part.
|
|
1170
|
+
* @returns Table definitions in relationship order.
|
|
1171
|
+
*/
|
|
1172
|
+
const readSheetTables = (entries, sheetPart) => {
|
|
1173
|
+
const relationships = readRelationships(entries, sheetPart);
|
|
1174
|
+
const tables = [];
|
|
1175
|
+
for (let index = 0; index < relationships.length; index += 1) {
|
|
1176
|
+
const relationship = relationships[index];
|
|
1177
|
+
if (!relationship.type.toLowerCase().endsWith("/table") || relationship.external) {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const xml = zipText(entries, relationship.target);
|
|
1181
|
+
if (xml === (void 0)) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
try {
|
|
1185
|
+
const scan = scanXml(xml);
|
|
1186
|
+
for (let tagIndex = 0; tagIndex < scan.tags.length; tagIndex += 1) {
|
|
1187
|
+
const tag = scan.tags[tagIndex];
|
|
1188
|
+
if (tag.closing || tag.local !== "table") {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const name = cleanLabel(xmlAttr(tag, "name"), "");
|
|
1192
|
+
const displayName = cleanLabel(xmlAttr(tag, "displayName"), name);
|
|
1193
|
+
const ref = cleanLabel(xmlAttr(tag, "ref"), "");
|
|
1194
|
+
if (displayName.length > 0) {
|
|
1195
|
+
tables.push({ name, displayName, ref });
|
|
1196
|
+
}
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
catch {
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return tables;
|
|
1205
|
+
};
|
|
1206
|
+
/**
|
|
1207
|
+
* Parse one worksheet: formulas and hyperlinks. Cell values and shared strings
|
|
1208
|
+
* are deliberately not emitted as outline nodes; the sheet/table/named-range
|
|
1209
|
+
* hierarchy is the structural contract for this format.
|
|
1210
|
+
*
|
|
1211
|
+
* @param context - Parse context.
|
|
1212
|
+
* @param entries - OOXML package parts.
|
|
1213
|
+
* @param sheet - Sheet definition.
|
|
1214
|
+
* @param externalTargets - External link index to target.
|
|
1215
|
+
* @returns Whether the worksheet part was readable.
|
|
1216
|
+
*/
|
|
1217
|
+
const parseWorksheet = (context, entries, sheet, externalTargets) => {
|
|
1218
|
+
const xml = zipText(entries, sheet.part);
|
|
1219
|
+
if (xml === (void 0)) {
|
|
1220
|
+
pushWarning(context, `excel: worksheet part ${sheet.part} is missing`);
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
const scan = scanXml(xml);
|
|
1224
|
+
const relationships = relationshipMap(readRelationships(entries, sheet.part));
|
|
1225
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
1226
|
+
const tag = scan.tags[index];
|
|
1227
|
+
if (tag.closing) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (tag.local === "c") {
|
|
1231
|
+
const address = xmlAttr(tag, "r");
|
|
1232
|
+
if (!/^[A-Z]{1,3}[1-9][0-9]*$/.test(address)) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const formula = xmlChildText(xml, scan, index, "f");
|
|
1236
|
+
if (formula.length > 0) {
|
|
1237
|
+
addFormulaReferences(context, formula, sheet.name, address, externalTargets);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
else if (tag.local === "hyperlink") {
|
|
1241
|
+
const relationshipId = xmlAttr(tag, "r:id");
|
|
1242
|
+
const location = xmlAttr(tag, "location");
|
|
1243
|
+
const relationship = relationships.get(relationshipId);
|
|
1244
|
+
if (relationship !== (void 0)) {
|
|
1245
|
+
addReference(context, {
|
|
1246
|
+
kind: "hyperlink",
|
|
1247
|
+
from: `${sheet.name}!${xmlAttr(tag, "ref")}`,
|
|
1248
|
+
target: relationship.target,
|
|
1249
|
+
detail: relationship.external ? "external hyperlink" : "internal hyperlink",
|
|
1250
|
+
});
|
|
1251
|
+
if (relationship.external) {
|
|
1252
|
+
addImport(context, relationship.target);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
else if (location.length > 0) {
|
|
1256
|
+
addReference(context, {
|
|
1257
|
+
kind: "hyperlink",
|
|
1258
|
+
from: `${sheet.name}!${xmlAttr(tag, "ref")}`,
|
|
1259
|
+
target: `#${location}`,
|
|
1260
|
+
detail: "internal hyperlink",
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return true;
|
|
1266
|
+
};
|
|
1267
|
+
/**
|
|
1268
|
+
* Parse an OOXML spreadsheet into sheet sections with table and named-range
|
|
1269
|
+
* children.
|
|
1270
|
+
*
|
|
1271
|
+
* @param context - Parse context.
|
|
1272
|
+
* @param entries - OOXML package parts.
|
|
1273
|
+
*/
|
|
1274
|
+
const parseExcel = (context, entries) => {
|
|
1275
|
+
const workbook = readWorkbook(context, entries);
|
|
1276
|
+
const sharedStrings = readSharedStrings(context, entries);
|
|
1277
|
+
const externalTargets = readExternalTargets(entries);
|
|
1278
|
+
for (let index = 0; index < externalTargets.size; index += 1) {
|
|
1279
|
+
const entry = Array.from(externalTargets.entries())[index];
|
|
1280
|
+
if (entry !== (void 0)) {
|
|
1281
|
+
addReference(context, { kind: "external-link", from: context.document.path, target: entry[1], detail: `external workbook [${entry[0]}]` });
|
|
1282
|
+
addImport(context, entry[1]);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (workbook.sheets.length === 0) {
|
|
1286
|
+
pushWarning(context, "excel: workbook defines no sheets");
|
|
1287
|
+
}
|
|
1288
|
+
for (let index = 0; index < workbook.sheets.length; index += 1) {
|
|
1289
|
+
const sheet = workbook.sheets[index];
|
|
1290
|
+
const sheetSection = appendSection(context, undefined, "sheet", sheet.name, 0);
|
|
1291
|
+
try {
|
|
1292
|
+
parseWorksheet(context, entries, sheet, externalTargets);
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
pushWarning(context, `excel: sheet ${sheet.name} could not be parsed (${error instanceof Error ? error.message : "parse failure"})`);
|
|
1296
|
+
}
|
|
1297
|
+
const tables = readSheetTables(entries, sheet.part);
|
|
1298
|
+
for (let tableIndex = 0; tableIndex < tables.length; tableIndex += 1) {
|
|
1299
|
+
const table = tables[tableIndex];
|
|
1300
|
+
if (sheetSection !== (void 0)) {
|
|
1301
|
+
appendSection(context, sheetSection, "table", table.displayName, 1);
|
|
1302
|
+
}
|
|
1303
|
+
addExport(context, table.displayName);
|
|
1304
|
+
}
|
|
1305
|
+
for (let nameIndex = 0; nameIndex < workbook.names.length; nameIndex += 1) {
|
|
1306
|
+
const defined = workbook.names[nameIndex];
|
|
1307
|
+
if (defined.localSheetIndex !== index) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (sheetSection !== (void 0)) {
|
|
1311
|
+
appendSection(context, sheetSection, "section", defined.name, 1);
|
|
1312
|
+
}
|
|
1313
|
+
addExport(context, defined.name);
|
|
1314
|
+
addReference(context, { kind: "named-range", from: context.document.path, target: defined.name, detail: defined.formula });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const globalNames = workbook.names.filter((name) => name.localSheetIndex < 0);
|
|
1318
|
+
if (globalNames.length > 0) {
|
|
1319
|
+
const namedSection = appendSection(context, undefined, "section", "Named ranges", 0);
|
|
1320
|
+
for (let index = 0; index < globalNames.length; index += 1) {
|
|
1321
|
+
const defined = globalNames[index];
|
|
1322
|
+
if (namedSection !== (void 0)) {
|
|
1323
|
+
appendSection(context, namedSection, "section", defined.name, 1);
|
|
1324
|
+
}
|
|
1325
|
+
addExport(context, defined.name);
|
|
1326
|
+
addReference(context, { kind: "named-range", from: context.document.path, target: defined.name, detail: defined.formula });
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
setMetadata(context, "excel.sheets", String(workbook.sheets.length));
|
|
1330
|
+
setMetadata(context, "excel.definedNames", String(workbook.names.length));
|
|
1331
|
+
setMetadata(context, "excel.sharedStrings", String(sharedStrings.length));
|
|
1332
|
+
pushWarning(context, "excel: formulas are not evaluated; cell values remain cached; shared/dynamic/external references beyond literal A1 syntax are not expanded");
|
|
1333
|
+
};
|
|
1334
|
+
/**
|
|
1335
|
+
* Read the slide order through presentation relationships, with a numeric
|
|
1336
|
+
* part-name fallback for packages that omit the relationship part.
|
|
1337
|
+
*
|
|
1338
|
+
* @param entries - OOXML package parts.
|
|
1339
|
+
* @returns Slides in presentation order.
|
|
1340
|
+
*/
|
|
1341
|
+
const readSlides = (entries) => {
|
|
1342
|
+
const xml = zipText(entries, "ppt/presentation.xml");
|
|
1343
|
+
const relationships = relationshipMap(readRelationships(entries, "ppt/presentation.xml"));
|
|
1344
|
+
const slides = [];
|
|
1345
|
+
if (xml !== (void 0)) {
|
|
1346
|
+
const scan = scanXml(xml);
|
|
1347
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
1348
|
+
const tag = scan.tags[index];
|
|
1349
|
+
if (tag.closing || tag.local !== "sldId") {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const relationId = xmlAttr(tag, "r:id");
|
|
1353
|
+
const relationship = relationships.get(relationId);
|
|
1354
|
+
if (relationship !== (void 0)) {
|
|
1355
|
+
slides.push({ relationId, part: relationship.target });
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (slides.length === 0) {
|
|
1360
|
+
const names = Array.from(entries.keys()).filter((name) => /^ppt\/slides\/slide[0-9]+\.xml$/.test(name));
|
|
1361
|
+
names.sort((left, right) => {
|
|
1362
|
+
const leftNumber = Number(/slide([0-9]+)\.xml$/.exec(left)?.[1] ?? 0);
|
|
1363
|
+
const rightNumber = Number(/slide([0-9]+)\.xml$/.exec(right)?.[1] ?? 0);
|
|
1364
|
+
return leftNumber - rightNumber;
|
|
1365
|
+
});
|
|
1366
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
1367
|
+
slides.push({ relationId: "", part: names[index] });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return slides;
|
|
1371
|
+
};
|
|
1372
|
+
/**
|
|
1373
|
+
* Extract slide text, title placeholders and slide relationships.
|
|
1374
|
+
*
|
|
1375
|
+
* @param context - Parse context.
|
|
1376
|
+
* @param entries - OOXML package parts.
|
|
1377
|
+
* @param slide - Slide part.
|
|
1378
|
+
* @param ordinal - One-based slide number.
|
|
1379
|
+
* @returns Nothing.
|
|
1380
|
+
*/
|
|
1381
|
+
const parseSlide = (context, entries, slide, ordinal) => {
|
|
1382
|
+
const xml = zipText(entries, slide.part);
|
|
1383
|
+
if (xml === (void 0)) {
|
|
1384
|
+
pushWarning(context, `powerpoint: slide part ${slide.part} is missing`);
|
|
1385
|
+
appendSection(context, undefined, "slide", `Slide ${ordinal}`, 0);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const scan = scanXml(xml);
|
|
1389
|
+
const shapes = [];
|
|
1390
|
+
const paragraphs = [];
|
|
1391
|
+
let shape;
|
|
1392
|
+
let paragraph;
|
|
1393
|
+
for (let index = 0; index < scan.tags.length; index += 1) {
|
|
1394
|
+
const tag = scan.tags[index];
|
|
1395
|
+
if (tag.local === "sp") {
|
|
1396
|
+
if (tag.closing) {
|
|
1397
|
+
if (shape !== (void 0)) {
|
|
1398
|
+
shapes.push({ title: shape.title, paragraphs: [...shape.paragraphs] });
|
|
1399
|
+
}
|
|
1400
|
+
shape = undefined;
|
|
1401
|
+
paragraph = undefined;
|
|
1402
|
+
}
|
|
1403
|
+
else if (!tag.selfClosing) {
|
|
1404
|
+
shape = { title: false, paragraphs: [] };
|
|
1405
|
+
}
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (shape !== (void 0) && tag.local === "ph" && !tag.closing) {
|
|
1409
|
+
const placeholder = xmlAttr(tag, "type");
|
|
1410
|
+
if (placeholder === "title" || placeholder === "ctrTitle") {
|
|
1411
|
+
shape.title = true;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (tag.local === "p" && !tag.name.startsWith("p:") && !tag.name.startsWith("p14:")) {
|
|
1415
|
+
if (tag.closing) {
|
|
1416
|
+
if (paragraph !== (void 0)) {
|
|
1417
|
+
const text = cleanLabel(paragraph.join(""), "");
|
|
1418
|
+
if (text.length > 0) {
|
|
1419
|
+
paragraphs.push(text);
|
|
1420
|
+
if (shape !== (void 0)) {
|
|
1421
|
+
shape.paragraphs.push(text);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
paragraph = undefined;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
else if (paragraph === (void 0) && !tag.selfClosing) {
|
|
1428
|
+
paragraph = [];
|
|
1429
|
+
}
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (paragraph !== (void 0) && tag.local === "t" && !tag.closing && !tag.name.startsWith("p:")) {
|
|
1433
|
+
const text = xmlInnerText(xml, scan, index);
|
|
1434
|
+
if (text.length > 0) {
|
|
1435
|
+
paragraph.push(text);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (paragraph !== (void 0)) {
|
|
1440
|
+
const text = cleanLabel(paragraph.join(""), "");
|
|
1441
|
+
if (text.length > 0) {
|
|
1442
|
+
paragraphs.push(text);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
let title = "";
|
|
1446
|
+
for (let index = 0; index < shapes.length; index += 1) {
|
|
1447
|
+
if (shapes[index].title && shapes[index].paragraphs.length > 0) {
|
|
1448
|
+
title = shapes[index].paragraphs.join(" ");
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const slideSection = appendSection(context, undefined, "slide", title.length > 0 ? title : `Slide ${ordinal}`, 0);
|
|
1453
|
+
if (slideSection !== (void 0)) {
|
|
1454
|
+
if (title.length > 0) {
|
|
1455
|
+
appendSection(context, slideSection, "heading", title, 1);
|
|
1456
|
+
}
|
|
1457
|
+
for (let index = 0; index < paragraphs.length; index += 1) {
|
|
1458
|
+
if (title.length > 0 && paragraphs[index] === title) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
appendSection(context, slideSection, "paragraph", paragraphs[index], 1);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const relationships = readRelationships(entries, slide.part);
|
|
1465
|
+
for (let index = 0; index < relationships.length; index += 1) {
|
|
1466
|
+
const relationship = relationships[index];
|
|
1467
|
+
const type = relationship.type.toLowerCase();
|
|
1468
|
+
if (type.endsWith("/hyperlink")) {
|
|
1469
|
+
addReference(context, {
|
|
1470
|
+
kind: "hyperlink",
|
|
1471
|
+
from: context.document.path,
|
|
1472
|
+
target: relationship.target,
|
|
1473
|
+
detail: relationship.external ? "external hyperlink" : "internal hyperlink",
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
else if (type.endsWith("/oleobject") || type.endsWith("/package") || type.endsWith("/embedded")) {
|
|
1477
|
+
addReference(context, { kind: "embedded", from: context.document.path, target: relationship.target, detail: "embedded object" });
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (relationship.external) {
|
|
1483
|
+
addImport(context, relationship.target);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
/**
|
|
1488
|
+
* Parse an OOXML presentation.
|
|
1489
|
+
*
|
|
1490
|
+
* @param context - Parse context.
|
|
1491
|
+
* @param entries - OOXML package parts.
|
|
1492
|
+
*/
|
|
1493
|
+
const parsePowerPoint = (context, entries) => {
|
|
1494
|
+
const slides = readSlides(entries);
|
|
1495
|
+
if (slides.length === 0) {
|
|
1496
|
+
pushWarning(context, "powerpoint: presentation contains no slides");
|
|
1497
|
+
}
|
|
1498
|
+
for (let index = 0; index < slides.length; index += 1) {
|
|
1499
|
+
const fallbackNumber = Number(/slide([0-9]+)\.xml$/.exec(slides[index].part)?.[1] ?? 0);
|
|
1500
|
+
const ordinal = slides[index].relationId.length === 0 && Number.isSafeInteger(fallbackNumber) && fallbackNumber > 0 ? fallbackNumber : index + 1;
|
|
1501
|
+
try {
|
|
1502
|
+
parseSlide(context, entries, slides[index], ordinal);
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
pushWarning(context, `powerpoint: slide ${ordinal} could not be parsed (${error instanceof Error ? error.message : "parse failure"})`);
|
|
1506
|
+
appendSection(context, undefined, "slide", `Slide ${ordinal}`, 0);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
setMetadata(context, "powerpoint.slides", String(slides.length));
|
|
1510
|
+
pushWarning(context, "powerpoint: slide order and text placeholders only; layouts, masters, notes, charts, SmartArt and images are not expanded");
|
|
1511
|
+
};
|
|
1512
|
+
/**
|
|
1513
|
+
* Whether a UTF-16 code unit can be part of readable text. Surrogates are
|
|
1514
|
+
* excluded because the scanner works on individual code units; unpaired
|
|
1515
|
+
* surrogates are a strong signal of binary data.
|
|
1516
|
+
*
|
|
1517
|
+
* @param code - UTF-16 code unit.
|
|
1518
|
+
* @returns Whether the unit is printable.
|
|
1519
|
+
*/
|
|
1520
|
+
const isPrintableCodeUnit = (code) => {
|
|
1521
|
+
if (code === 0x09) {
|
|
1522
|
+
return true;
|
|
1523
|
+
}
|
|
1524
|
+
if (code < 0x20 || code === 0x7f) {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
if (code >= 0xd800 && code <= 0xdfff) {
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
if (code === 0xfeff || code === 0xfffe || code === 0xffff) {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
return true;
|
|
1534
|
+
};
|
|
1535
|
+
/**
|
|
1536
|
+
* Count Unicode letters without constructing a code-point array.
|
|
1537
|
+
*
|
|
1538
|
+
* @param text - Candidate text.
|
|
1539
|
+
* @returns Letter count.
|
|
1540
|
+
*/
|
|
1541
|
+
const letterCount = (text) => {
|
|
1542
|
+
const matches = text.match(/\p{L}/gu);
|
|
1543
|
+
return matches === null ? 0 : matches.length;
|
|
1544
|
+
};
|
|
1545
|
+
/**
|
|
1546
|
+
* Detect ASCII bytes that were misread as UTF-16LE. When every code unit has
|
|
1547
|
+
* printable ASCII in both byte lanes, the "text" is an ASCII run viewed with
|
|
1548
|
+
* the wrong endianness (for example a Jet header), not real CJK text. Rejecting
|
|
1549
|
+
* this shape removes the biggest false-positive class from whole-file scans
|
|
1550
|
+
* without dropping legitimate UTF-16LE or CJK content.
|
|
1551
|
+
*
|
|
1552
|
+
* @param text - Candidate fragment.
|
|
1553
|
+
* @returns Whether every code unit looks like two printable ASCII bytes.
|
|
1554
|
+
*/
|
|
1555
|
+
const looksByteSwappedAscii = (text) => {
|
|
1556
|
+
const chars = Array.from(text);
|
|
1557
|
+
if (chars.length < 4) {
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
let swapped = 0;
|
|
1561
|
+
for (let index = 0; index < chars.length; index += 1) {
|
|
1562
|
+
const code = chars[index].codePointAt(0);
|
|
1563
|
+
if (code === (void 0) || code > 0xffff) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
const high = code >>> 8;
|
|
1567
|
+
const low = code & 0xff;
|
|
1568
|
+
if (high >= 0x20 && high <= 0x7e && low >= 0x20 && low <= 0x7e) {
|
|
1569
|
+
swapped += 1;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return swapped === chars.length;
|
|
1573
|
+
};
|
|
1574
|
+
/**
|
|
1575
|
+
* Whether a fragment is likely to be human-readable prose or a name rather
|
|
1576
|
+
* than binary noise. The threshold favours precision: legacy formats contain
|
|
1577
|
+
* many short ASCII runs inside structures, and false paragraphs are worse than
|
|
1578
|
+
* a missed line.
|
|
1579
|
+
*
|
|
1580
|
+
* @param text - Normalized fragment.
|
|
1581
|
+
* @returns Whether to keep it.
|
|
1582
|
+
*/
|
|
1583
|
+
const isMeaningfulFragment = (text) => {
|
|
1584
|
+
if (text.length < 3) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
if (looksByteSwappedAscii(text)) {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
const visible = text.replace(/\s/g, "").length;
|
|
1591
|
+
if (visible < 3) {
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
const letters = letterCount(text);
|
|
1595
|
+
if (letters < 2 || letters / visible < 0.35) {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
const lower = text.toLowerCase();
|
|
1599
|
+
if (lower === "root entry" ||
|
|
1600
|
+
lower === "compobj" ||
|
|
1601
|
+
lower === "summaryinformation" ||
|
|
1602
|
+
lower === "documentsummaryinformation" ||
|
|
1603
|
+
lower.startsWith("standard jet db") ||
|
|
1604
|
+
lower.startsWith("standard ace db") ||
|
|
1605
|
+
lower.startsWith("microsoft ")) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
return true;
|
|
1609
|
+
};
|
|
1610
|
+
/**
|
|
1611
|
+
* Scan UTF-16LE runs at one alignment.
|
|
1612
|
+
*
|
|
1613
|
+
* @param bytes - Stream bytes.
|
|
1614
|
+
* @param parity - Starting byte offset (0 or 1).
|
|
1615
|
+
* @param sink - Collected fragments.
|
|
1616
|
+
* @returns Nothing; fragments are appended to `sink`.
|
|
1617
|
+
*/
|
|
1618
|
+
const scanUtf16 = (bytes, parity, sink) => {
|
|
1619
|
+
let runStart = -1;
|
|
1620
|
+
let run = "";
|
|
1621
|
+
const flush = () => {
|
|
1622
|
+
if (run.length >= 3) {
|
|
1623
|
+
sink.push({ text: run, offset: runStart });
|
|
1624
|
+
}
|
|
1625
|
+
runStart = -1;
|
|
1626
|
+
run = "";
|
|
1627
|
+
};
|
|
1628
|
+
for (let index = parity; index + 1 < bytes.length; index += 2) {
|
|
1629
|
+
if (sink.length >= MaxRawFragments) {
|
|
1630
|
+
flush();
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
const code = bytes[index] | (bytes[index + 1] << 8);
|
|
1634
|
+
if (isPrintableCodeUnit(code)) {
|
|
1635
|
+
if (runStart < 0) {
|
|
1636
|
+
runStart = index;
|
|
1637
|
+
}
|
|
1638
|
+
run += String.fromCharCode(code);
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
flush();
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
flush();
|
|
1645
|
+
};
|
|
1646
|
+
/**
|
|
1647
|
+
* Scan printable ASCII runs.
|
|
1648
|
+
*
|
|
1649
|
+
* @param bytes - Stream bytes.
|
|
1650
|
+
* @param sink - Collected fragments.
|
|
1651
|
+
* @returns Nothing; fragments are appended to `sink`.
|
|
1652
|
+
*/
|
|
1653
|
+
const scanAscii = (bytes, sink) => {
|
|
1654
|
+
let runStart = -1;
|
|
1655
|
+
let run = "";
|
|
1656
|
+
const flush = () => {
|
|
1657
|
+
if (run.length >= 3) {
|
|
1658
|
+
sink.push({ text: run, offset: runStart });
|
|
1659
|
+
}
|
|
1660
|
+
runStart = -1;
|
|
1661
|
+
run = "";
|
|
1662
|
+
};
|
|
1663
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
1664
|
+
if (sink.length >= MaxRawFragments) {
|
|
1665
|
+
flush();
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const code = bytes[index];
|
|
1669
|
+
if (code >= 0x20 && code <= 0x7e) {
|
|
1670
|
+
if (runStart < 0) {
|
|
1671
|
+
runStart = index;
|
|
1672
|
+
}
|
|
1673
|
+
run += String.fromCharCode(code);
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
flush();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
flush();
|
|
1680
|
+
};
|
|
1681
|
+
/**
|
|
1682
|
+
* Recover printable text from a binary stream. UTF-16LE is tried at both byte
|
|
1683
|
+
* alignments, then ASCII; fragments are ordered by their source offset and
|
|
1684
|
+
* deduplicated after normalization. The 500-fragment ceiling is the
|
|
1685
|
+
* "do not turn a binary file into a wall of noise" bound.
|
|
1686
|
+
*
|
|
1687
|
+
* @param bytes - Stream bytes.
|
|
1688
|
+
* @returns Ordered fragments and whether raw scanning hit its own cap.
|
|
1689
|
+
*/
|
|
1690
|
+
const extractTextFragments = (bytes) => {
|
|
1691
|
+
const raw = [];
|
|
1692
|
+
scanUtf16(bytes, 0, raw);
|
|
1693
|
+
/**
|
|
1694
|
+
* The odd alignment is only a fallback. Scanning it unconditionally turns
|
|
1695
|
+
* every UTF-16LE string into a second, byte-swapped "CJK" duplicate that
|
|
1696
|
+
* passes the letter-ratio filter; that is binary noise, not document text.
|
|
1697
|
+
*/
|
|
1698
|
+
if (raw.length === 0) {
|
|
1699
|
+
scanUtf16(bytes, 1, raw);
|
|
1700
|
+
}
|
|
1701
|
+
if (raw.length < MaxRawFragments) {
|
|
1702
|
+
scanAscii(bytes, raw);
|
|
1703
|
+
}
|
|
1704
|
+
const truncated = raw.length >= MaxRawFragments;
|
|
1705
|
+
raw.sort((left, right) => left.offset - right.offset);
|
|
1706
|
+
const fragments = [];
|
|
1707
|
+
const seen = new Set();
|
|
1708
|
+
for (let index = 0; index < raw.length && fragments.length < MaxTextFragments; index += 1) {
|
|
1709
|
+
const text = raw[index].text.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
1710
|
+
if (!isMeaningfulFragment(text) || seen.has(text)) {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
seen.add(text);
|
|
1714
|
+
fragments.push({ text, offset: raw[index].offset });
|
|
1715
|
+
}
|
|
1716
|
+
return { fragments, truncated };
|
|
1717
|
+
};
|
|
1718
|
+
/**
|
|
1719
|
+
* Recover text from named streams, falling back to every stream when none of
|
|
1720
|
+
* the preferred names exist. Total scanned bytes are bounded independently of
|
|
1721
|
+
* the CFB stream limit.
|
|
1722
|
+
*
|
|
1723
|
+
* @param streams - Stream map from `readCfb`.
|
|
1724
|
+
* @param preferred - Stream names to try first.
|
|
1725
|
+
* @returns Ordered fragments and whether scanning was truncated.
|
|
1726
|
+
*/
|
|
1727
|
+
const streamText = (streams, preferred) => {
|
|
1728
|
+
const selected = [];
|
|
1729
|
+
for (let index = 0; index < preferred.length; index += 1) {
|
|
1730
|
+
const stream = streams.get(preferred[index]);
|
|
1731
|
+
if (stream !== (void 0)) {
|
|
1732
|
+
selected.push(stream);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (selected.length === 0) {
|
|
1736
|
+
streams.forEach((stream) => {
|
|
1737
|
+
selected.push(stream);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
const fragments = [];
|
|
1741
|
+
let scanned = 0;
|
|
1742
|
+
let truncated = false;
|
|
1743
|
+
for (let index = 0; index < selected.length; index += 1) {
|
|
1744
|
+
const stream = selected[index];
|
|
1745
|
+
if (scanned >= MaxTextScanBytes) {
|
|
1746
|
+
truncated = true;
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
const allowed = stream.length > MaxTextScanBytes - scanned ? stream.subarray(0, MaxTextScanBytes - scanned) : stream;
|
|
1750
|
+
scanned += allowed.length;
|
|
1751
|
+
const extracted = extractTextFragments(allowed);
|
|
1752
|
+
if (extracted.truncated) {
|
|
1753
|
+
truncated = true;
|
|
1754
|
+
}
|
|
1755
|
+
for (let fragmentIndex = 0; fragmentIndex < extracted.fragments.length && fragments.length < MaxTextFragments; fragmentIndex += 1) {
|
|
1756
|
+
const fragment = extracted.fragments[fragmentIndex];
|
|
1757
|
+
if (!fragments.some((candidate) => candidate.text === fragment.text)) {
|
|
1758
|
+
fragments.push(fragment);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return { fragments, truncated };
|
|
1763
|
+
};
|
|
1764
|
+
/**
|
|
1765
|
+
* Add URL references recovered from legacy text. A URL is the one structured
|
|
1766
|
+
* relationship that survives text extraction reliably; everything else in a
|
|
1767
|
+
* legacy binary is reported as heuristic.
|
|
1768
|
+
*
|
|
1769
|
+
* @param context - Parse context.
|
|
1770
|
+
* @param fragments - Recovered fragments.
|
|
1771
|
+
*/
|
|
1772
|
+
const addUrlReferences = (context, fragments) => {
|
|
1773
|
+
const pattern = /https?:\/\/[^\s"'<>)\]]+/g;
|
|
1774
|
+
for (let index = 0; index < fragments.length; index += 1) {
|
|
1775
|
+
let match = pattern.exec(fragments[index].text);
|
|
1776
|
+
while (match !== null) {
|
|
1777
|
+
addReference(context, { kind: "hyperlink", from: context.document.path, target: match[0], detail: "legacy hyperlink" });
|
|
1778
|
+
addImport(context, match[0]);
|
|
1779
|
+
match = pattern.exec(fragments[index].text);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
/**
|
|
1784
|
+
* Build one text-oriented section with paragraph children. A section is only
|
|
1785
|
+
* created when there is at least one printable fragment: an empty container or
|
|
1786
|
+
* a stream with no recoverable text must not look like a parsed document
|
|
1787
|
+
* section.
|
|
1788
|
+
*
|
|
1789
|
+
* @param context - Parse context.
|
|
1790
|
+
* @param label - Section label.
|
|
1791
|
+
* @param fragments - Paragraph fragments.
|
|
1792
|
+
* @returns The created section, or `undefined` when there is no text or the
|
|
1793
|
+
* section budget is exhausted.
|
|
1794
|
+
*/
|
|
1795
|
+
const appendTextSection = (context, label, fragments) => {
|
|
1796
|
+
if (fragments.length === 0) {
|
|
1797
|
+
pushWarning(context, `${label}: no printable text recovered`);
|
|
1798
|
+
return undefined;
|
|
1799
|
+
}
|
|
1800
|
+
const section = appendSection(context, undefined, "section", label, 0);
|
|
1801
|
+
if (section === (void 0)) {
|
|
1802
|
+
return undefined;
|
|
1803
|
+
}
|
|
1804
|
+
for (let index = 0; index < fragments.length; index += 1) {
|
|
1805
|
+
appendSection(context, section, "paragraph", fragments[index].text, 1);
|
|
1806
|
+
}
|
|
1807
|
+
return section;
|
|
1808
|
+
};
|
|
1809
|
+
/**
|
|
1810
|
+
* Read BIFF `BOUNDSHEET` records (0x0085) from an Excel workbook stream. Sheet
|
|
1811
|
+
* names are the only Excel structure the legacy reader claims; cells and
|
|
1812
|
+
* formulas are not decoded.
|
|
1813
|
+
*
|
|
1814
|
+
* @param stream - Workbook or Book stream.
|
|
1815
|
+
* @returns Sheet names in workbook order.
|
|
1816
|
+
*/
|
|
1817
|
+
const biffSheetNames = (stream) => {
|
|
1818
|
+
const names = [];
|
|
1819
|
+
const seen = new Set();
|
|
1820
|
+
if (stream.length < 4) {
|
|
1821
|
+
return names;
|
|
1822
|
+
}
|
|
1823
|
+
const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
|
|
1824
|
+
let offset = 0;
|
|
1825
|
+
let records = 0;
|
|
1826
|
+
while (offset + 4 <= stream.length && records < 20000) {
|
|
1827
|
+
const type = view.getUint16(offset, true);
|
|
1828
|
+
const length = view.getUint16(offset + 2, true);
|
|
1829
|
+
const dataStart = offset + 4;
|
|
1830
|
+
if (dataStart + length > stream.length) {
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
if (type === 0x0085 && length >= 8) {
|
|
1834
|
+
const count = stream[dataStart + 6];
|
|
1835
|
+
const flags = stream[dataStart + 7];
|
|
1836
|
+
if (count > 0 && count <= 255) {
|
|
1837
|
+
const nameStart = dataStart + 8;
|
|
1838
|
+
const wide = (flags & 0x01) !== 0;
|
|
1839
|
+
const byteLength = wide ? count * 2 : count;
|
|
1840
|
+
if (nameStart + byteLength <= stream.length) {
|
|
1841
|
+
let name = "";
|
|
1842
|
+
for (let index = 0; index < count; index += 1) {
|
|
1843
|
+
const code = wide ? stream[nameStart + index * 2] | (stream[nameStart + index * 2 + 1] << 8) : stream[nameStart + index];
|
|
1844
|
+
if (code === 0 || code < 0x20) {
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
name += String.fromCharCode(code);
|
|
1848
|
+
}
|
|
1849
|
+
const clean = cleanLabel(name, "");
|
|
1850
|
+
if (clean.length > 0 && !seen.has(clean)) {
|
|
1851
|
+
seen.add(clean);
|
|
1852
|
+
names.push(clean);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
offset = dataStart + length;
|
|
1858
|
+
records += 1;
|
|
1859
|
+
}
|
|
1860
|
+
return names;
|
|
1861
|
+
};
|
|
1862
|
+
/**
|
|
1863
|
+
* Parse a legacy OLE Office document. Text is recovered from the stream that
|
|
1864
|
+
* carries it in each format, then split into one section per meaningful
|
|
1865
|
+
* fragment. No binary field is interpreted as a document object unless the
|
|
1866
|
+
* format's own stream names identify it.
|
|
1867
|
+
*
|
|
1868
|
+
* @param context - Parse context.
|
|
1869
|
+
* @param streams - CFB streams.
|
|
1870
|
+
* @param kind - Document family.
|
|
1871
|
+
*/
|
|
1872
|
+
const parseLegacy = (context, streams, kind) => {
|
|
1873
|
+
if (kind === "excel") {
|
|
1874
|
+
const workbook = streams.get("Workbook") ?? streams.get("Book");
|
|
1875
|
+
const sheetNames = workbook === (void 0) ? [] : biffSheetNames(workbook);
|
|
1876
|
+
for (let index = 0; index < sheetNames.length; index += 1) {
|
|
1877
|
+
appendSection(context, undefined, "sheet", sheetNames[index], 0);
|
|
1878
|
+
}
|
|
1879
|
+
const text = streamText(streams, ["Workbook", "Book"]);
|
|
1880
|
+
appendTextSection(context, "Workbook text", text.fragments);
|
|
1881
|
+
addUrlReferences(context, text.fragments);
|
|
1882
|
+
if (text.truncated) {
|
|
1883
|
+
pushWarning(context, "excel: legacy text scan reached its fragment limit");
|
|
1884
|
+
}
|
|
1885
|
+
setMetadata(context, "excel.sheets", String(sheetNames.length));
|
|
1886
|
+
pushWarning(context, "excel: legacy BIFF text extraction is heuristic; cells, formulas and styles are not decoded");
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (kind === "word") {
|
|
1890
|
+
const text = streamText(streams, ["WordDocument"]);
|
|
1891
|
+
appendTextSection(context, "Word document", text.fragments);
|
|
1892
|
+
addUrlReferences(context, text.fragments);
|
|
1893
|
+
if (text.truncated) {
|
|
1894
|
+
pushWarning(context, "word: legacy text scan reached its fragment limit");
|
|
1895
|
+
}
|
|
1896
|
+
pushWarning(context, "word: legacy .doc text extraction is heuristic; tables, fields and formatting are not decoded");
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
if (kind === "powerpoint") {
|
|
1900
|
+
const text = streamText(streams, ["PowerPoint Document"]);
|
|
1901
|
+
appendTextSection(context, "PowerPoint presentation", text.fragments);
|
|
1902
|
+
addUrlReferences(context, text.fragments);
|
|
1903
|
+
if (text.truncated) {
|
|
1904
|
+
pushWarning(context, "powerpoint: legacy text scan reached its fragment limit");
|
|
1905
|
+
}
|
|
1906
|
+
pushWarning(context, "powerpoint: legacy .ppt text extraction is heuristic; slides, layouts and shapes are not decoded");
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (kind === "publisher") {
|
|
1910
|
+
const text = streamText(streams, ["Contents", "Quill", "QuillSub"]);
|
|
1911
|
+
appendTextSection(context, "Publisher document", text.fragments);
|
|
1912
|
+
addUrlReferences(context, text.fragments);
|
|
1913
|
+
if (text.truncated) {
|
|
1914
|
+
pushWarning(context, "publisher: legacy text scan reached its fragment limit");
|
|
1915
|
+
}
|
|
1916
|
+
pushWarning(context, "publisher: Publisher support is best-effort; text story order, pages, frames and layout are not decoded");
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const text = streamText(streams, []);
|
|
1920
|
+
appendTextSection(context, "OLE streams", text.fragments);
|
|
1921
|
+
addUrlReferences(context, text.fragments);
|
|
1922
|
+
pushWarning(context, "ole: unrecognized document family; only printable text fragments were recovered");
|
|
1923
|
+
};
|
|
1924
|
+
/**
|
|
1925
|
+
* Access/Jet engine magic strings after the four-byte file header.
|
|
1926
|
+
*/
|
|
1927
|
+
const AccessMagic = ["Standard Jet DB", "Standard ACE DB"];
|
|
1928
|
+
/**
|
|
1929
|
+
* Detect a Jet/ACE database by its header. Access files are deliberately not
|
|
1930
|
+
* treated as CFB: `.mdb` and `.accdb` use a page-oriented format with this
|
|
1931
|
+
* signature at offset 4.
|
|
1932
|
+
*
|
|
1933
|
+
* @param bytes - Candidate file bytes.
|
|
1934
|
+
* @returns Engine name, or `undefined` when the header is not an Access header.
|
|
1935
|
+
*/
|
|
1936
|
+
const accessEngine = (bytes) => {
|
|
1937
|
+
if (bytes.length < 24 || bytes[0] !== 0x00 || bytes[1] !== 0x01 || bytes[2] !== 0x00 || bytes[3] !== 0x00) {
|
|
1938
|
+
return undefined;
|
|
1939
|
+
}
|
|
1940
|
+
const magic = asciiAt(bytes, 4, 20);
|
|
1941
|
+
for (let index = 0; index < AccessMagic.length; index += 1) {
|
|
1942
|
+
if (magic.startsWith(AccessMagic[index])) {
|
|
1943
|
+
return AccessMagic[index];
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return undefined;
|
|
1947
|
+
};
|
|
1948
|
+
/**
|
|
1949
|
+
* Parse an Access database on a best-effort basis. There is no page parser
|
|
1950
|
+
* here, so catalog names are recovered from UTF-16LE identifier-shaped string
|
|
1951
|
+
* runs and classified only by a conventional prefix. The warning states that
|
|
1952
|
+
* limitation explicitly; no table schema or query SQL is claimed.
|
|
1953
|
+
*
|
|
1954
|
+
* @param context - Parse context.
|
|
1955
|
+
* @param bytes - Database bytes.
|
|
1956
|
+
* @param engine - Detected engine magic.
|
|
1957
|
+
*/
|
|
1958
|
+
const parseAccess = (context, bytes, engine) => {
|
|
1959
|
+
pushWarning(context, "access catalog best-effort: catalog names are recovered from string runs, not from table/page parsing");
|
|
1960
|
+
setMetadata(context, "access.engine", engine);
|
|
1961
|
+
/**
|
|
1962
|
+
* Skip the header and the first database page. Its ASCII engine magic and
|
|
1963
|
+
* fixed fields would otherwise be read as UTF-16LE and surface as catalog
|
|
1964
|
+
* candidates in an otherwise empty database.
|
|
1965
|
+
*/
|
|
1966
|
+
const scanStart = Math.min(bytes.length, 4096);
|
|
1967
|
+
const scanEnd = Math.min(bytes.length, scanStart + MaxTextScanBytes);
|
|
1968
|
+
const scanned = extractTextFragments(bytes.subarray(scanStart, scanEnd));
|
|
1969
|
+
const reserved = [
|
|
1970
|
+
"select",
|
|
1971
|
+
"from",
|
|
1972
|
+
"where",
|
|
1973
|
+
"table",
|
|
1974
|
+
"index",
|
|
1975
|
+
"primary",
|
|
1976
|
+
"foreign",
|
|
1977
|
+
"database",
|
|
1978
|
+
"access",
|
|
1979
|
+
"admin",
|
|
1980
|
+
"system",
|
|
1981
|
+
"standard",
|
|
1982
|
+
"create",
|
|
1983
|
+
"insert",
|
|
1984
|
+
"update",
|
|
1985
|
+
"delete",
|
|
1986
|
+
"query",
|
|
1987
|
+
"form",
|
|
1988
|
+
"report",
|
|
1989
|
+
];
|
|
1990
|
+
const candidates = [];
|
|
1991
|
+
const seen = new Set();
|
|
1992
|
+
for (let index = 0; index < scanned.fragments.length && candidates.length < 100; index += 1) {
|
|
1993
|
+
const name = scanned.fragments[index].text;
|
|
1994
|
+
if (!/^[\p{L}_][\p{L}\p{N}_ ]{1,62}$/u.test(name) || name.split(" ").length > 4) {
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
const lower = name.toLowerCase();
|
|
1998
|
+
if (reserved.includes(lower) || lower.startsWith("msys") || lower.startsWith("~") || seen.has(lower)) {
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
seen.add(lower);
|
|
2002
|
+
candidates.push(name);
|
|
2003
|
+
}
|
|
2004
|
+
if (candidates.length === 0) {
|
|
2005
|
+
pushWarning(context, "access: no catalog names could be recovered");
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
const catalog = appendSection(context, undefined, "section", "Access catalog (best effort)", 0);
|
|
2009
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
2010
|
+
const kind = /^(?:qry|q_)/i.test(candidates[index]) ? "query" : "table";
|
|
2011
|
+
if (catalog !== (void 0)) {
|
|
2012
|
+
appendSection(context, catalog, kind, candidates[index], 1);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
setMetadata(context, "access.catalogCandidates", String(candidates.length));
|
|
2017
|
+
if (scanned.truncated) {
|
|
2018
|
+
pushWarning(context, "access: catalog scan reached its fragment limit");
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
/**
|
|
2022
|
+
* Determine the OOXML family from package part names when the extension is
|
|
2023
|
+
* unknown or absent.
|
|
2024
|
+
*
|
|
2025
|
+
* @param entries - OOXML package parts.
|
|
2026
|
+
* @returns Best family guess.
|
|
2027
|
+
*/
|
|
2028
|
+
const kindFromParts = (entries) => {
|
|
2029
|
+
let word = false;
|
|
2030
|
+
let excel = false;
|
|
2031
|
+
let powerpoint = false;
|
|
2032
|
+
const names = Array.from(entries.keys());
|
|
2033
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
2034
|
+
if (names[index].startsWith("word/")) {
|
|
2035
|
+
word = true;
|
|
2036
|
+
}
|
|
2037
|
+
else if (names[index].startsWith("xl/")) {
|
|
2038
|
+
excel = true;
|
|
2039
|
+
}
|
|
2040
|
+
else if (names[index].startsWith("ppt/")) {
|
|
2041
|
+
powerpoint = true;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (word) {
|
|
2045
|
+
return "word";
|
|
2046
|
+
}
|
|
2047
|
+
if (excel) {
|
|
2048
|
+
return "excel";
|
|
2049
|
+
}
|
|
2050
|
+
if (powerpoint) {
|
|
2051
|
+
return "powerpoint";
|
|
2052
|
+
}
|
|
2053
|
+
return "unknown";
|
|
2054
|
+
};
|
|
2055
|
+
/**
|
|
2056
|
+
* Determine the legacy family from CFB stream names.
|
|
2057
|
+
*
|
|
2058
|
+
* @param streams - CFB streams.
|
|
2059
|
+
* @returns Best family guess.
|
|
2060
|
+
*/
|
|
2061
|
+
const kindFromStreams = (streams) => {
|
|
2062
|
+
if (streams.has("WordDocument")) {
|
|
2063
|
+
return "word";
|
|
2064
|
+
}
|
|
2065
|
+
if (streams.has("Workbook") || streams.has("Book")) {
|
|
2066
|
+
return "excel";
|
|
2067
|
+
}
|
|
2068
|
+
if (streams.has("PowerPoint Document")) {
|
|
2069
|
+
return "powerpoint";
|
|
2070
|
+
}
|
|
2071
|
+
if (streams.has("Contents") || streams.has("Quill") || streams.has("QuillSub")) {
|
|
2072
|
+
return "publisher";
|
|
2073
|
+
}
|
|
2074
|
+
return "unknown";
|
|
2075
|
+
};
|
|
2076
|
+
/**
|
|
2077
|
+
* Whether a byte sequence starts with a ZIP header.
|
|
2078
|
+
*
|
|
2079
|
+
* A package normally starts with a local-file header (`PK\x03\x04`), but an
|
|
2080
|
+
* empty archive starts with its end-of-central-directory record (`PK\x05\x06`)
|
|
2081
|
+
* and a spanned archive with the spanning marker (`PK\x07\x08`). All three are
|
|
2082
|
+
* valid ZIP starts, so all three are routed to the OOXML reader, which then
|
|
2083
|
+
* reports an empty package instead of "unrecognized container".
|
|
2084
|
+
*
|
|
2085
|
+
* @param bytes - Candidate bytes.
|
|
2086
|
+
* @returns Whether the ZIP magic is present.
|
|
2087
|
+
*/
|
|
2088
|
+
const hasZipMagic = (bytes) => bytes.length >= 4 &&
|
|
2089
|
+
bytes[0] === 0x50 &&
|
|
2090
|
+
bytes[1] === 0x4b &&
|
|
2091
|
+
((bytes[2] === 0x03 && bytes[3] === 0x04) || (bytes[2] === 0x05 && bytes[3] === 0x06) || (bytes[2] === 0x07 && bytes[3] === 0x08));
|
|
2092
|
+
/**
|
|
2093
|
+
* Whether a byte sequence starts with the CFB signature.
|
|
2094
|
+
*
|
|
2095
|
+
* @param bytes - Candidate bytes.
|
|
2096
|
+
* @returns Whether the CFB magic is present.
|
|
2097
|
+
*/
|
|
2098
|
+
const hasCfbMagic = (bytes) => bytes.length >= 8 &&
|
|
2099
|
+
bytes[0] === 0xd0 &&
|
|
2100
|
+
bytes[1] === 0xcf &&
|
|
2101
|
+
bytes[2] === 0x11 &&
|
|
2102
|
+
bytes[3] === 0xe0 &&
|
|
2103
|
+
bytes[4] === 0xa1 &&
|
|
2104
|
+
bytes[5] === 0xb1 &&
|
|
2105
|
+
bytes[6] === 0x1a &&
|
|
2106
|
+
bytes[7] === 0xe1;
|
|
2107
|
+
/**
|
|
2108
|
+
* Parse Office bytes that have already been read and validated by a caller.
|
|
2109
|
+
*
|
|
2110
|
+
* The container level is decided by magic, not extension: a `.docx` that is
|
|
2111
|
+
* really a CFB file is still treated as a CFB container, and a ZIP with an
|
|
2112
|
+
* unhelpful extension is sniffed from its parts. Extension only disambiguates
|
|
2113
|
+
* the document family inside a valid container.
|
|
2114
|
+
*
|
|
2115
|
+
* @param path - Root-relative path used in references and IDs.
|
|
2116
|
+
* @param bytes - Complete file bytes.
|
|
2117
|
+
* @returns Best-effort document.
|
|
2118
|
+
*/
|
|
2119
|
+
const parseOfficeBytes = (path, bytes) => {
|
|
2120
|
+
const extensionKind = kindFromPath(path);
|
|
2121
|
+
const format = hasZipMagic(bytes) ? "ooxml" : hasCfbMagic(bytes) || accessEngine(bytes) !== (void 0) ? "ole" : "unknown";
|
|
2122
|
+
const context = createContext(path, extensionKind, format, bytes);
|
|
2123
|
+
try {
|
|
2124
|
+
if (format === "ooxml") {
|
|
2125
|
+
let entries;
|
|
2126
|
+
try {
|
|
2127
|
+
entries = readZipEntries(bytes);
|
|
2128
|
+
}
|
|
2129
|
+
catch (error) {
|
|
2130
|
+
if (isFilesystemError(error) && error.code === "quota_exceeded") {
|
|
2131
|
+
throw error;
|
|
2132
|
+
}
|
|
2133
|
+
pushWarning(context, `ooxml container rejected: ${error instanceof Error ? error.message : "parse failure"}`);
|
|
2134
|
+
}
|
|
2135
|
+
if (entries !== (void 0)) {
|
|
2136
|
+
const kind = extensionKind === "unknown" ? kindFromParts(entries) : extensionKind;
|
|
2137
|
+
context.document.kind = kind;
|
|
2138
|
+
try {
|
|
2139
|
+
readDocumentProperties(context, entries);
|
|
2140
|
+
}
|
|
2141
|
+
catch (error) {
|
|
2142
|
+
pushWarning(context, `office properties ignored (${error instanceof Error ? error.message : "parse failure"})`);
|
|
2143
|
+
}
|
|
2144
|
+
try {
|
|
2145
|
+
if (kind === "word") {
|
|
2146
|
+
parseWord(context, entries);
|
|
2147
|
+
}
|
|
2148
|
+
else if (kind === "excel") {
|
|
2149
|
+
parseExcel(context, entries);
|
|
2150
|
+
}
|
|
2151
|
+
else if (kind === "powerpoint") {
|
|
2152
|
+
parsePowerPoint(context, entries);
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
pushWarning(context, "ooxml package does not contain word/, xl/ or ppt/ parts");
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
catch (error) {
|
|
2159
|
+
pushWarning(context, `ooxml ${kind} structure ignored (${error instanceof Error ? error.message : "parse failure"})`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
else {
|
|
2163
|
+
context.document.kind = extensionKind;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
else if (format === "ole") {
|
|
2167
|
+
const engine = accessEngine(bytes);
|
|
2168
|
+
if (engine !== (void 0)) {
|
|
2169
|
+
context.document.kind = "access";
|
|
2170
|
+
parseAccess(context, bytes, engine);
|
|
2171
|
+
}
|
|
2172
|
+
else {
|
|
2173
|
+
const cfb = readCfb(bytes);
|
|
2174
|
+
for (let index = 0; index < cfb.warnings.length; index += 1) {
|
|
2175
|
+
pushWarning(context, cfb.warnings[index]);
|
|
2176
|
+
}
|
|
2177
|
+
const kind = extensionKind === "unknown" ? kindFromStreams(cfb.streams) : extensionKind;
|
|
2178
|
+
context.document.kind = kind;
|
|
2179
|
+
if (cfb.streams.size === 0) {
|
|
2180
|
+
pushWarning(context, "ole container has no readable streams");
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
try {
|
|
2184
|
+
parseLegacy(context, cfb.streams, kind);
|
|
2185
|
+
}
|
|
2186
|
+
catch (error) {
|
|
2187
|
+
pushWarning(context, `ole ${kind} structure ignored (${error instanceof Error ? error.message : "parse failure"})`);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
else {
|
|
2193
|
+
context.document.kind = "unknown";
|
|
2194
|
+
pushWarning(context, "unrecognized office container; no structure was extracted");
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
catch (error) {
|
|
2198
|
+
if (isFilesystemError(error)) {
|
|
2199
|
+
throw error;
|
|
2200
|
+
}
|
|
2201
|
+
pushWarning(context, `office parse stopped: ${error instanceof Error ? error.message : "unknown failure"}`);
|
|
2202
|
+
}
|
|
2203
|
+
if (context.truncated) {
|
|
2204
|
+
pushWarning(context, `office section limit ${MaxSections} reached; remaining structure was not parsed`);
|
|
2205
|
+
}
|
|
2206
|
+
return context.document;
|
|
2207
|
+
};
|
|
2208
|
+
/**
|
|
2209
|
+
* Parse one Office document, reading its bytes through the session sandbox.
|
|
2210
|
+
*
|
|
2211
|
+
* A file larger than 64 MiB is refused with `quota_exceeded` before it is read.
|
|
2212
|
+
* Path resolution, symlink checks and file-type checks are delegated to
|
|
2213
|
+
* `PathSandbox`, so this parser cannot read outside the session roots.
|
|
2214
|
+
*
|
|
2215
|
+
* @param sandbox - Session sandbox that owns path resolution.
|
|
2216
|
+
* @param path - Root-relative POSIX path.
|
|
2217
|
+
* @returns Best-effort document structure and warnings.
|
|
2218
|
+
*/
|
|
2219
|
+
export const parseOffice = (sandbox, path) => {
|
|
2220
|
+
const rooted = sandbox.resolve(path, "read");
|
|
2221
|
+
sandbox.revalidate(rooted);
|
|
2222
|
+
const stat = statOrFail(rooted.absolute);
|
|
2223
|
+
if (!stat.isFile()) {
|
|
2224
|
+
return fail("not_file");
|
|
2225
|
+
}
|
|
2226
|
+
if (stat.size > OfficeMaxDocumentBytes) {
|
|
2227
|
+
return fail("quota_exceeded");
|
|
2228
|
+
}
|
|
2229
|
+
sandbox.revalidate(rooted);
|
|
2230
|
+
const bytes = readBytesOrFail(rooted.absolute);
|
|
2231
|
+
if (bytes.byteLength > OfficeMaxDocumentBytes) {
|
|
2232
|
+
return fail("quota_exceeded");
|
|
2233
|
+
}
|
|
2234
|
+
return parseOfficeBytes(rooted.relative, bytes);
|
|
2235
|
+
};
|
|
2236
|
+
/**
|
|
2237
|
+
* Map an Office section kind to the closest existing outline kind. The outline
|
|
2238
|
+
* vocabulary predates Office and has no `sheet`, `slide`, `table`, `query`,
|
|
2239
|
+
* `form` or `report` values, so those become `section` rather than inventing
|
|
2240
|
+
* enum members outside the shared type.
|
|
2241
|
+
*
|
|
2242
|
+
* @param kind - Office section kind.
|
|
2243
|
+
* @returns Outline kind.
|
|
2244
|
+
*/
|
|
2245
|
+
const outlineKindOf = (kind) => {
|
|
2246
|
+
if (kind === "heading") {
|
|
2247
|
+
return "heading";
|
|
2248
|
+
}
|
|
2249
|
+
if (kind === "paragraph") {
|
|
2250
|
+
return "paragraph";
|
|
2251
|
+
}
|
|
2252
|
+
return "section";
|
|
2253
|
+
};
|
|
2254
|
+
/**
|
|
2255
|
+
* Number of sections in one subtree, including the subtree root.
|
|
2256
|
+
*
|
|
2257
|
+
* @param section - Office section.
|
|
2258
|
+
* @returns Subtree section count.
|
|
2259
|
+
*/
|
|
2260
|
+
const countSections = (section) => {
|
|
2261
|
+
let total = 1;
|
|
2262
|
+
for (let index = 0; index < section.children.length; index += 1) {
|
|
2263
|
+
total += countSections(section.children[index]);
|
|
2264
|
+
}
|
|
2265
|
+
return total;
|
|
2266
|
+
};
|
|
2267
|
+
/**
|
|
2268
|
+
* Highest document ordinal in one subtree. A parent's range ends at the last
|
|
2269
|
+
* ordinal of its deepest descendant, which keeps ranges nested and monotonic.
|
|
2270
|
+
*
|
|
2271
|
+
* @param section - Office section.
|
|
2272
|
+
* @returns Highest ordinal in the subtree.
|
|
2273
|
+
*/
|
|
2274
|
+
const lastSectionIndex = (section) => {
|
|
2275
|
+
let last = section.index;
|
|
2276
|
+
for (let index = 0; index < section.children.length; index += 1) {
|
|
2277
|
+
last = Math.max(last, lastSectionIndex(section.children[index]));
|
|
2278
|
+
}
|
|
2279
|
+
return last;
|
|
2280
|
+
};
|
|
2281
|
+
/**
|
|
2282
|
+
* Build outline nodes from Office sections.
|
|
2283
|
+
*
|
|
2284
|
+
* Ranges are synthetic because Office formats expose neither text lines nor
|
|
2285
|
+
* part offsets. Two monotonic projections keep the tree usable:
|
|
2286
|
+
*
|
|
2287
|
+
* - `startLine`/`endLine` are the section's document ordinals: `index + 1` up
|
|
2288
|
+
* to the highest ordinal in its subtree, so a parent covers its children and
|
|
2289
|
+
* siblings never overlap;
|
|
2290
|
+
* - `startByte`/`endByte` project the same ordinal interval onto the container
|
|
2291
|
+
* size, so the sections partition the file exactly once and a parent still
|
|
2292
|
+
* covers its subtree.
|
|
2293
|
+
*
|
|
2294
|
+
* Both are explicitly approximate: they answer "where in document order", not
|
|
2295
|
+
* "at which OOXML part offset". The path is `"<file>#<section-id>"` and every
|
|
2296
|
+
* node is a symbol, not a file anchor.
|
|
2297
|
+
*
|
|
2298
|
+
* The raw-byte entry point enforces `OfficeMaxOutlineBytes` itself so a direct
|
|
2299
|
+
* caller sees `quota_exceeded` rather than handing a container to a text
|
|
2300
|
+
* charset detector (which would report `binary_file`). `buildOutline` applies
|
|
2301
|
+
* the same ceiling before it reads the file.
|
|
2302
|
+
*
|
|
2303
|
+
* @param input - Path and complete container bytes.
|
|
2304
|
+
* @returns Outline nodes in document order.
|
|
2305
|
+
*/
|
|
2306
|
+
export const officeOutlineParser = (input) => {
|
|
2307
|
+
if (input.bytes.byteLength > OfficeMaxOutlineBytes) {
|
|
2308
|
+
return fail("quota_exceeded");
|
|
2309
|
+
}
|
|
2310
|
+
const document = parseOfficeBytes(input.path, input.bytes);
|
|
2311
|
+
const totalBytes = input.bytes.byteLength;
|
|
2312
|
+
let sectionCount = 0;
|
|
2313
|
+
for (let index = 0; index < document.sections.length; index += 1) {
|
|
2314
|
+
sectionCount += countSections(document.sections[index]);
|
|
2315
|
+
}
|
|
2316
|
+
const byteAt = (ordinal) => sectionCount === 0 ? 0n : (BigInt(totalBytes) * BigInt(Math.max(0, Math.min(ordinal, sectionCount)))) / BigInt(sectionCount);
|
|
2317
|
+
const build = (section) => {
|
|
2318
|
+
const last = lastSectionIndex(section);
|
|
2319
|
+
const children = [];
|
|
2320
|
+
for (let index = 0; index < section.children.length; index += 1) {
|
|
2321
|
+
children.push(build(section.children[index]));
|
|
2322
|
+
}
|
|
2323
|
+
const node = {
|
|
2324
|
+
isFile: false,
|
|
2325
|
+
isDirectory: false,
|
|
2326
|
+
path: `${input.path}#${section.id}`,
|
|
2327
|
+
type: outlineKindOf(section.kind),
|
|
2328
|
+
range: {
|
|
2329
|
+
startByte: byteAt(section.index),
|
|
2330
|
+
endByte: byteAt(last + 1),
|
|
2331
|
+
startLine: BigInt(section.index + 1),
|
|
2332
|
+
endLine: BigInt(last + 1),
|
|
2333
|
+
},
|
|
2334
|
+
label: section.label,
|
|
2335
|
+
depth: section.depth,
|
|
2336
|
+
children,
|
|
2337
|
+
};
|
|
2338
|
+
return node;
|
|
2339
|
+
};
|
|
2340
|
+
const nodes = [];
|
|
2341
|
+
for (let index = 0; index < document.sections.length; index += 1) {
|
|
2342
|
+
nodes.push(build(document.sections[index]));
|
|
2343
|
+
}
|
|
2344
|
+
return nodes;
|
|
2345
|
+
};
|
|
2346
|
+
/**
|
|
2347
|
+
* Install the Office outline extractor into the shared strategy registry.
|
|
2348
|
+
* Idempotent: repeated calls leave the same parser registered.
|
|
2349
|
+
*/
|
|
2350
|
+
export const installOfficeOutlineParser = () => {
|
|
2351
|
+
if (!hasOfficeOutlineParser()) {
|
|
2352
|
+
registerOfficeOutlineParser(officeOutlineParser);
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
/**
|
|
2356
|
+
* External targets of a parsed document, for dependency/import consumers.
|
|
2357
|
+
*
|
|
2358
|
+
* @param document - Parsed Office document.
|
|
2359
|
+
* @returns A copy of the import list.
|
|
2360
|
+
*/
|
|
2361
|
+
export const officeImports = (document) => [...document.imports];
|
|
2362
|
+
/**
|
|
2363
|
+
* Exported names of a parsed document, for export consumers.
|
|
2364
|
+
*
|
|
2365
|
+
* @param document - Parsed Office document.
|
|
2366
|
+
* @returns A copy of the export list.
|
|
2367
|
+
*/
|
|
2368
|
+
export const officeExports = (document) => [...document.exports];
|
|
2369
|
+
/**
|
|
2370
|
+
* Outbound references of a parsed document, for dependency consumers.
|
|
2371
|
+
*
|
|
2372
|
+
* @param document - Parsed Office document.
|
|
2373
|
+
* @returns A copy of the reference list.
|
|
2374
|
+
*/
|
|
2375
|
+
export const officeReferences = (document) => document.references.map((reference) => ({ ...reference }));
|