@preapexis/piexis 0.1.2
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/.github/workflows/npm-publish.yml +63 -0
- package/AGENTS.md +58 -0
- package/PERMISSION_MODES_PLAN.md +279 -0
- package/README.md +207 -0
- package/WHAT_YOU_GET.md +153 -0
- package/eval/README.md +72 -0
- package/eval/permission-cases.json +28 -0
- package/eval/permissions.mjs +103 -0
- package/extensions/ask-question.ts +111 -0
- package/extensions/background-tasks.ts +433 -0
- package/extensions/background-worker.ts +50 -0
- package/extensions/glob.ts +32 -0
- package/extensions/mode.ts +397 -0
- package/extensions/ui.ts +134 -0
- package/lib/bash-approvals.ts +113 -0
- package/lib/bash-policy.ts +164 -0
- package/lib/mode-policy.ts +174 -0
- package/lib/path-policy.ts +182 -0
- package/lib/permission-classifier.ts +128 -0
- package/lib/permission-config.ts +91 -0
- package/lib/permission-denials.ts +32 -0
- package/lib/permission-intent.ts +128 -0
- package/lib/permission-taxonomy.ts +15 -0
- package/lib/task-broker.ts +65 -0
- package/lib/task-checkout.ts +39 -0
- package/lib/task-git.ts +44 -0
- package/lib/task-patch.ts +106 -0
- package/lib/task-permissions.ts +51 -0
- package/lib/task-report.ts +62 -0
- package/lib/task-worktree-policy.ts +56 -0
- package/package.json +51 -0
- package/prompts/commit.md +7 -0
- package/prompts/init.md +66 -0
- package/prompts/summary.md +14 -0
- package/skills/claude-api/LICENSE.txt +202 -0
- package/skills/claude-api/SKILL.md +572 -0
- package/skills/claude-api/csharp/claude-api/README.md +362 -0
- package/skills/claude-api/csharp/claude-api/batches.md +14 -0
- package/skills/claude-api/csharp/claude-api/files-api.md +25 -0
- package/skills/claude-api/csharp/claude-api/streaming.md +28 -0
- package/skills/claude-api/csharp/claude-api/tool-use.md +164 -0
- package/skills/claude-api/curl/examples.md +256 -0
- package/skills/claude-api/curl/managed-agents.md +371 -0
- package/skills/claude-api/go/claude-api/README.md +188 -0
- package/skills/claude-api/go/claude-api/files-api.md +23 -0
- package/skills/claude-api/go/claude-api/streaming.md +43 -0
- package/skills/claude-api/go/claude-api/tool-use.md +220 -0
- package/skills/claude-api/go/managed-agents/README.md +564 -0
- package/skills/claude-api/java/claude-api/README.md +238 -0
- package/skills/claude-api/java/claude-api/files-api.md +27 -0
- package/skills/claude-api/java/claude-api/streaming.md +24 -0
- package/skills/claude-api/java/claude-api/tool-use.md +228 -0
- package/skills/claude-api/java/managed-agents/README.md +443 -0
- package/skills/claude-api/php/claude-api/README.md +174 -0
- package/skills/claude-api/php/claude-api/batches.md +15 -0
- package/skills/claude-api/php/claude-api/files-api.md +13 -0
- package/skills/claude-api/php/claude-api/streaming.md +27 -0
- package/skills/claude-api/php/claude-api/tool-use.md +253 -0
- package/skills/claude-api/php/managed-agents/README.md +443 -0
- package/skills/claude-api/python/claude-api/README.md +565 -0
- package/skills/claude-api/python/claude-api/batches.md +198 -0
- package/skills/claude-api/python/claude-api/files-api.md +170 -0
- package/skills/claude-api/python/claude-api/sdk-upgrade.md +286 -0
- package/skills/claude-api/python/claude-api/streaming.md +179 -0
- package/skills/claude-api/python/claude-api/tool-use.md +627 -0
- package/skills/claude-api/python/managed-agents/README.md +335 -0
- package/skills/claude-api/ruby/claude-api/README.md +134 -0
- package/skills/claude-api/ruby/claude-api/streaming.md +16 -0
- package/skills/claude-api/ruby/claude-api/tool-use.md +41 -0
- package/skills/claude-api/ruby/managed-agents/README.md +394 -0
- package/skills/claude-api/shared/admin-api.md +179 -0
- package/skills/claude-api/shared/agent-design.md +101 -0
- package/skills/claude-api/shared/anthropic-cli.md +269 -0
- package/skills/claude-api/shared/claude-platform-on-aws.md +59 -0
- package/skills/claude-api/shared/cost-optimization.md +233 -0
- package/skills/claude-api/shared/error-codes.md +262 -0
- package/skills/claude-api/shared/live-sources.md +166 -0
- package/skills/claude-api/shared/managed-agents-api-reference.md +455 -0
- package/skills/claude-api/shared/managed-agents-client-patterns.md +220 -0
- package/skills/claude-api/shared/managed-agents-core.md +359 -0
- package/skills/claude-api/shared/managed-agents-environments.md +226 -0
- package/skills/claude-api/shared/managed-agents-events.md +290 -0
- package/skills/claude-api/shared/managed-agents-memory.md +201 -0
- package/skills/claude-api/shared/managed-agents-multiagent.md +260 -0
- package/skills/claude-api/shared/managed-agents-onboarding.md +82 -0
- package/skills/claude-api/shared/managed-agents-outcomes.md +108 -0
- package/skills/claude-api/shared/managed-agents-overview.md +77 -0
- package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +156 -0
- package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +282 -0
- package/skills/claude-api/shared/managed-agents-tools.md +504 -0
- package/skills/claude-api/shared/managed-agents-webhooks.md +143 -0
- package/skills/claude-api/shared/model-migration.md +1872 -0
- package/skills/claude-api/shared/models.md +142 -0
- package/skills/claude-api/shared/platform-availability.md +53 -0
- package/skills/claude-api/shared/prompt-audit.md +223 -0
- package/skills/claude-api/shared/prompt-caching.md +292 -0
- package/skills/claude-api/shared/token-counting.md +56 -0
- package/skills/claude-api/shared/tool-use-concepts.md +526 -0
- package/skills/claude-api/typescript/claude-api/README.md +405 -0
- package/skills/claude-api/typescript/claude-api/batches.md +106 -0
- package/skills/claude-api/typescript/claude-api/files-api.md +98 -0
- package/skills/claude-api/typescript/claude-api/streaming.md +178 -0
- package/skills/claude-api/typescript/claude-api/tool-use.md +586 -0
- package/skills/claude-api/typescript/managed-agents/README.md +359 -0
- package/skills/code-review/SKILL.md +34 -0
- package/skills/code-review/agents/openai.yaml +3 -0
- package/skills/codebase-design/DEEPENING.md +37 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/skills/codebase-design/SKILL.md +40 -0
- package/skills/diagnosing-bugs/SKILL.md +38 -0
- package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/skills/docx/LICENSE.txt +30 -0
- package/skills/docx/SKILL.md +93 -0
- package/skills/docx/scripts/__init__.py +1 -0
- package/skills/docx/scripts/accept_changes.py +135 -0
- package/skills/docx/scripts/comment.py +368 -0
- package/skills/docx/scripts/merge_runs.py +310 -0
- package/skills/docx/scripts/office/helpers/__init__.py +111 -0
- package/skills/docx/scripts/office/helpers/pptx_chart.py +170 -0
- package/skills/docx/scripts/office/helpers/pptx_slide.py +60 -0
- package/skills/docx/scripts/office/helpers/pptx_theme.py +114 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/docx/scripts/office/soffice.py +192 -0
- package/skills/docx/scripts/office/validate.py +173 -0
- package/skills/docx/scripts/office/validators/__init__.py +15 -0
- package/skills/docx/scripts/office/validators/base.py +875 -0
- package/skills/docx/scripts/office/validators/docx.py +466 -0
- package/skills/docx/scripts/office/validators/pptx.py +441 -0
- package/skills/docx/scripts/office/validators/redlining.py +299 -0
- package/skills/docx/scripts/templates/comments.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/docx/scripts/templates/people.xml +3 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +27 -0
- package/skills/grilling/SKILL.md +41 -0
- package/skills/lean-build/SKILL.md +22 -0
- package/skills/lean-build/agents/openai.yaml +4 -0
- package/skills/mcp-builder/LICENSE.txt +202 -0
- package/skills/mcp-builder/SKILL.md +235 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/migration/SKILL.md +17 -0
- package/skills/migration/agents/openai.yaml +4 -0
- package/skills/pdf/LICENSE.txt +30 -0
- package/skills/pdf/SKILL.md +27 -0
- package/skills/pdf/forms.md +294 -0
- package/skills/pdf/reference.md +612 -0
- package/skills/pdf/scripts/check_bounding_boxes.py +65 -0
- package/skills/pdf/scripts/check_fillable_fields.py +11 -0
- package/skills/pdf/scripts/convert_pdf_to_images.py +33 -0
- package/skills/pdf/scripts/create_validation_image.py +37 -0
- package/skills/pdf/scripts/extract_form_field_info.py +122 -0
- package/skills/pdf/scripts/extract_form_structure.py +115 -0
- package/skills/pdf/scripts/fill_fillable_fields.py +98 -0
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/skills/pptx/LICENSE.txt +30 -0
- package/skills/pptx/SKILL.md +240 -0
- package/skills/pptx/scripts/__init__.py +0 -0
- package/skills/pptx/scripts/add_slide.py +367 -0
- package/skills/pptx/scripts/clean.py +309 -0
- package/skills/pptx/scripts/office/helpers/__init__.py +111 -0
- package/skills/pptx/scripts/office/helpers/pptx_chart.py +170 -0
- package/skills/pptx/scripts/office/helpers/pptx_slide.py +60 -0
- package/skills/pptx/scripts/office/helpers/pptx_theme.py +114 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/pptx/scripts/office/soffice.py +192 -0
- package/skills/pptx/scripts/office/validate.py +173 -0
- package/skills/pptx/scripts/office/validators/__init__.py +15 -0
- package/skills/pptx/scripts/office/validators/base.py +875 -0
- package/skills/pptx/scripts/office/validators/docx.py +466 -0
- package/skills/pptx/scripts/office/validators/pptx.py +441 -0
- package/skills/pptx/scripts/office/validators/redlining.py +299 -0
- package/skills/pptx/scripts/thumbnail.py +311 -0
- package/skills/safe-refactor/SKILL.md +16 -0
- package/skills/safe-refactor/agents/openai.yaml +4 -0
- package/skills/tdd/SKILL.md +18 -0
- package/skills/tdd/agents/openai.yaml +3 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/tests.md +77 -0
- package/skills/verify-and-stop/SKILL.md +16 -0
- package/skills/verify-and-stop/agents/openai.yaml +4 -0
- package/skills/webapp-testing/LICENSE.txt +202 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/skills/writing-for-agents/SKILL.md +28 -0
- package/skills/writing-for-agents/agents/openai.yaml +3 -0
- package/skills/xlsx/LICENSE.txt +30 -0
- package/skills/xlsx/SKILL.md +101 -0
- package/skills/xlsx/scripts/office/helpers/__init__.py +111 -0
- package/skills/xlsx/scripts/office/helpers/pptx_chart.py +170 -0
- package/skills/xlsx/scripts/office/helpers/pptx_slide.py +60 -0
- package/skills/xlsx/scripts/office/helpers/pptx_theme.py +114 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/xlsx/scripts/office/soffice.py +192 -0
- package/skills/xlsx/scripts/office/validate.py +173 -0
- package/skills/xlsx/scripts/office/validators/__init__.py +15 -0
- package/skills/xlsx/scripts/office/validators/base.py +875 -0
- package/skills/xlsx/scripts/office/validators/docx.py +466 -0
- package/skills/xlsx/scripts/office/validators/pptx.py +441 -0
- package/skills/xlsx/scripts/office/validators/redlining.py +299 -0
- package/skills/xlsx/scripts/recalc.py +308 -0
- package/skills-lock.json +359 -0
- package/tests/auto-permissions.test.mjs +315 -0
- package/tests/background-tasks.test.mjs +27 -0
- package/tests/background-worker.test.mjs +60 -0
- package/tests/bash-permissions.test.mjs +276 -0
- package/tests/glob.test.mjs +31 -0
- package/tests/mode.test.mjs +62 -0
- package/tests/permission-evaluation.test.mjs +70 -0
- package/tests/permission-foundation.test.mjs +194 -0
- package/tests/permission-policy.test.mjs +158 -0
- package/tests/runtime/README.md +51 -0
- package/tests/runtime/harness.mjs +190 -0
- package/tests/runtime/permissions.test.mjs +192 -0
- package/tests/runtime/tui.test.mjs +60 -0
- package/tests/skills-routing.test.mjs +51 -0
- package/tests/task-integration.test.mjs +324 -0
- package/tests/task-permissions.test.mjs +178 -0
- package/tests/task-worktree-policy.test.mjs +53 -0
- package/themes/orange.json +82 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Merge adjacent identically-formatted runs in a DOCX.
|
|
2
|
+
|
|
3
|
+
Word fragments paragraph text across many <w:r> elements (revision ids,
|
|
4
|
+
spell-check markers, editing history), which makes find-and-replace on
|
|
5
|
+
word/document.xml unreliable — the string you're looking for is split
|
|
6
|
+
across runs. This coalesces adjacent runs whose formatting (<w:rPr>) is
|
|
7
|
+
identical, strips rsid attributes and proofErr markers, and consolidates the
|
|
8
|
+
text elements — <w:t>, and <w:delText> for text inside a tracked deletion.
|
|
9
|
+
|
|
10
|
+
Rendering is unchanged. The text you search is what Word draws, which is not
|
|
11
|
+
always the bytes in the file: an element without xml:space="preserve" has its
|
|
12
|
+
edge whitespace trimmed before it reaches the page, so `<w:t>Hello </w:t>`
|
|
13
|
+
followed by `<w:t>world</w:t>` reads "Helloworld" and merges to exactly that.
|
|
14
|
+
|
|
15
|
+
Runs in two different <w:ins>/<w:del> wrappers are never merged: that would
|
|
16
|
+
rewrite tracked-change structure, collapsing separate revisions into one.
|
|
17
|
+
|
|
18
|
+
Only word/document.xml is processed (not headers, footers, or footnotes).
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
python merge_runs.py unpacked/ # after unzip, before editing
|
|
22
|
+
python merge_runs.py document.docx # rewrite in place
|
|
23
|
+
python merge_runs.py document.docx -o out.docx
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import sys
|
|
29
|
+
import tempfile
|
|
30
|
+
import zipfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import defusedxml.minidom
|
|
34
|
+
|
|
35
|
+
from office.helpers import XML_SPACE, rendered_text, rezip, safe_extract
|
|
36
|
+
|
|
37
|
+
WORDML_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def merge_runs(input_dir: str) -> tuple[int, str]:
|
|
41
|
+
doc_xml = Path(input_dir) / "word" / "document.xml"
|
|
42
|
+
|
|
43
|
+
if not doc_xml.exists():
|
|
44
|
+
return 0, f"Error: {doc_xml} not found"
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
|
|
48
|
+
root = dom.documentElement
|
|
49
|
+
run_names = _run_tag_names(root)
|
|
50
|
+
|
|
51
|
+
_remove_elements(root, "proofErr")
|
|
52
|
+
|
|
53
|
+
runs = _find_runs(root, run_names)
|
|
54
|
+
_strip_rsid_attrs(runs)
|
|
55
|
+
|
|
56
|
+
merge_count = 0
|
|
57
|
+
for container in {run.parentNode for run in runs}:
|
|
58
|
+
merge_count += _merge_runs_in(container, run_names)
|
|
59
|
+
|
|
60
|
+
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
|
|
61
|
+
return merge_count, f"Merged {merge_count} runs"
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return 0, f"Error: {e}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_element(node, tag: str) -> bool:
|
|
70
|
+
name = node.localName or node.tagName
|
|
71
|
+
return name == tag or name.endswith(f":{tag}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _run_tag_names(root) -> set[str]:
|
|
75
|
+
names = set()
|
|
76
|
+
for attr in root.attributes.values():
|
|
77
|
+
if attr.value == WORDML_NS:
|
|
78
|
+
if attr.name == "xmlns":
|
|
79
|
+
names.add("r")
|
|
80
|
+
elif attr.name.startswith("xmlns:"):
|
|
81
|
+
names.add(attr.name.split(":", 1)[1] + ":r")
|
|
82
|
+
return names or {"w:r", "r"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _find_elements(root, tag: str) -> list:
|
|
86
|
+
results = []
|
|
87
|
+
|
|
88
|
+
def traverse(node):
|
|
89
|
+
if node.nodeType == node.ELEMENT_NODE:
|
|
90
|
+
if _is_element(node, tag):
|
|
91
|
+
results.append(node)
|
|
92
|
+
for child in node.childNodes:
|
|
93
|
+
traverse(child)
|
|
94
|
+
|
|
95
|
+
traverse(root)
|
|
96
|
+
return results
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _find_runs(root, run_names: set[str]) -> list:
|
|
100
|
+
return [e for e in _find_elements(root, "r") if _is_run(e, run_names)]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_child(parent, tag: str):
|
|
104
|
+
return next(iter(_get_children(parent, tag)), None)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_children(parent, tag: str) -> list:
|
|
108
|
+
return [
|
|
109
|
+
child
|
|
110
|
+
for child in parent.childNodes
|
|
111
|
+
if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _is_adjacent(elem1, elem2) -> bool:
|
|
116
|
+
node = elem1.nextSibling
|
|
117
|
+
while node:
|
|
118
|
+
if node == elem2:
|
|
119
|
+
return True
|
|
120
|
+
if node.nodeType == node.ELEMENT_NODE:
|
|
121
|
+
return False
|
|
122
|
+
if node.nodeType == node.TEXT_NODE and node.data.strip(XML_SPACE):
|
|
123
|
+
return False
|
|
124
|
+
node = node.nextSibling
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _remove_elements(root, tag: str):
|
|
131
|
+
for elem in _find_elements(root, tag):
|
|
132
|
+
if elem.parentNode:
|
|
133
|
+
elem.parentNode.removeChild(elem)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _strip_rsid_attrs(runs: list):
|
|
137
|
+
for run in runs:
|
|
138
|
+
for attr in list(run.attributes.values()):
|
|
139
|
+
if "rsid" in attr.name.lower():
|
|
140
|
+
run.removeAttribute(attr.name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _merge_runs_in(container, run_names: set[str]) -> int:
|
|
146
|
+
merge_count = 0
|
|
147
|
+
run = _first_child_run(container, run_names)
|
|
148
|
+
|
|
149
|
+
while run:
|
|
150
|
+
while True:
|
|
151
|
+
next_elem = _next_element_sibling(run)
|
|
152
|
+
if next_elem and _is_run(next_elem, run_names) and _can_merge(run, next_elem):
|
|
153
|
+
_merge_run_content(run, next_elem)
|
|
154
|
+
container.removeChild(next_elem)
|
|
155
|
+
merge_count += 1
|
|
156
|
+
else:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
_consolidate_text(run)
|
|
160
|
+
run = _next_sibling_run(run, run_names)
|
|
161
|
+
|
|
162
|
+
return merge_count
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _first_child_run(container, run_names: set[str]):
|
|
166
|
+
for child in container.childNodes:
|
|
167
|
+
if child.nodeType == child.ELEMENT_NODE and _is_run(child, run_names):
|
|
168
|
+
return child
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _next_element_sibling(node):
|
|
173
|
+
sibling = node.nextSibling
|
|
174
|
+
while sibling:
|
|
175
|
+
if sibling.nodeType == sibling.ELEMENT_NODE:
|
|
176
|
+
return sibling
|
|
177
|
+
sibling = sibling.nextSibling
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _next_sibling_run(node, run_names: set[str]):
|
|
182
|
+
sibling = node.nextSibling
|
|
183
|
+
while sibling:
|
|
184
|
+
if sibling.nodeType == sibling.ELEMENT_NODE:
|
|
185
|
+
if _is_run(sibling, run_names):
|
|
186
|
+
return sibling
|
|
187
|
+
sibling = sibling.nextSibling
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _is_run(node, run_names: set[str]) -> bool:
|
|
192
|
+
return node.tagName in run_names
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _can_merge(run1, run2) -> bool:
|
|
196
|
+
rpr1 = _get_child(run1, "rPr")
|
|
197
|
+
rpr2 = _get_child(run2, "rPr")
|
|
198
|
+
|
|
199
|
+
if (rpr1 is None) != (rpr2 is None):
|
|
200
|
+
return False
|
|
201
|
+
if rpr1 is None:
|
|
202
|
+
return True
|
|
203
|
+
return rpr1.toxml() == rpr2.toxml()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _merge_run_content(target, source):
|
|
207
|
+
for child in list(source.childNodes):
|
|
208
|
+
if child.nodeType == child.ELEMENT_NODE:
|
|
209
|
+
name = child.localName or child.tagName
|
|
210
|
+
if name != "rPr" and not name.endswith(":rPr"):
|
|
211
|
+
target.appendChild(child)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _element_text(elem) -> str:
|
|
215
|
+
return "".join(
|
|
216
|
+
child.data
|
|
217
|
+
for child in elem.childNodes
|
|
218
|
+
if child.nodeType in (child.TEXT_NODE, child.CDATA_SECTION_NODE)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _has_preserve(elem) -> bool:
|
|
223
|
+
return elem.getAttribute("xml:space") == "preserve"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _rendered_text(elem) -> str:
|
|
227
|
+
return rendered_text(_element_text(elem), _has_preserve(elem))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _consolidate_text(run):
|
|
231
|
+
for tag in ("t", "delText"):
|
|
232
|
+
_consolidate_text_elements(run, tag)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _consolidate_text_elements(run, tag: str):
|
|
236
|
+
t_elements = _get_children(run, tag)
|
|
237
|
+
|
|
238
|
+
for i in range(len(t_elements) - 1, 0, -1):
|
|
239
|
+
curr, prev = t_elements[i], t_elements[i - 1]
|
|
240
|
+
|
|
241
|
+
if _is_adjacent(prev, curr):
|
|
242
|
+
merged = _rendered_text(prev) + _rendered_text(curr)
|
|
243
|
+
had_preserve = _has_preserve(prev) or _has_preserve(curr)
|
|
244
|
+
|
|
245
|
+
new_text = run.ownerDocument.createTextNode(merged)
|
|
246
|
+
for node in list(prev.childNodes):
|
|
247
|
+
if node.nodeType in (node.TEXT_NODE, node.CDATA_SECTION_NODE):
|
|
248
|
+
prev.removeChild(node)
|
|
249
|
+
else:
|
|
250
|
+
run.insertBefore(node, curr)
|
|
251
|
+
prev.appendChild(new_text)
|
|
252
|
+
for node in list(curr.childNodes):
|
|
253
|
+
if node.nodeType not in (node.TEXT_NODE, node.CDATA_SECTION_NODE):
|
|
254
|
+
run.insertBefore(node, curr)
|
|
255
|
+
|
|
256
|
+
if merged != merged.strip(XML_SPACE) or had_preserve:
|
|
257
|
+
prev.setAttribute("xml:space", "preserve")
|
|
258
|
+
elif prev.hasAttribute("xml:space"):
|
|
259
|
+
prev.removeAttribute("xml:space")
|
|
260
|
+
|
|
261
|
+
run.removeChild(curr)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _merge_or_die(path: Path) -> str:
|
|
267
|
+
_, msg = merge_runs(str(path))
|
|
268
|
+
if msg.startswith("Error"):
|
|
269
|
+
print(msg, file=sys.stderr)
|
|
270
|
+
sys.exit(1)
|
|
271
|
+
return msg
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main() -> None:
|
|
275
|
+
p = argparse.ArgumentParser(
|
|
276
|
+
description="Merge adjacent identically-formatted runs in a DOCX (directory or .docx file)."
|
|
277
|
+
)
|
|
278
|
+
p.add_argument("input", help="Unpacked DOCX directory OR a .docx/.dotx file")
|
|
279
|
+
p.add_argument(
|
|
280
|
+
"-o", "--output",
|
|
281
|
+
help="Output .docx path (only valid when input is a .docx; default: overwrite input)",
|
|
282
|
+
)
|
|
283
|
+
args = p.parse_args()
|
|
284
|
+
|
|
285
|
+
src = Path(args.input)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
if src.is_dir():
|
|
289
|
+
if args.output:
|
|
290
|
+
p.error("--output is only valid for .docx input; directory input is modified in place")
|
|
291
|
+
print(_merge_or_die(src))
|
|
292
|
+
elif src.is_file() and src.suffix.lower() in (".docx", ".dotx"):
|
|
293
|
+
out = Path(args.output) if args.output else src
|
|
294
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
295
|
+
tmp_path = Path(tmp)
|
|
296
|
+
with zipfile.ZipFile(src) as zf:
|
|
297
|
+
safe_extract(zf, tmp_path)
|
|
298
|
+
msg = _merge_or_die(tmp_path)
|
|
299
|
+
rezip(tmp_path, out)
|
|
300
|
+
print(f"{msg}; wrote {out}")
|
|
301
|
+
else:
|
|
302
|
+
print(f"Error: {src} is neither a directory nor a .docx/.dotx file", file=sys.stderr)
|
|
303
|
+
sys.exit(1)
|
|
304
|
+
except (OSError, ValueError, zipfile.BadZipFile) as e:
|
|
305
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
306
|
+
sys.exit(1)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
main()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import posixpath
|
|
3
|
+
import re
|
|
4
|
+
import stat
|
|
5
|
+
import tempfile
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
OOXML_FAMILY = {
|
|
11
|
+
".docx": "docx",
|
|
12
|
+
".dotx": "docx",
|
|
13
|
+
".pptx": "pptx",
|
|
14
|
+
".potx": "pptx",
|
|
15
|
+
".xlsx": "xlsx",
|
|
16
|
+
".xltx": "xlsx",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:")
|
|
20
|
+
|
|
21
|
+
SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def opc_target(target: str, source_part: str, target_mode: str = "") -> str | None:
|
|
25
|
+
if not target:
|
|
26
|
+
return None
|
|
27
|
+
if target_mode.lower() == "external":
|
|
28
|
+
return None
|
|
29
|
+
if _SCHEME_RE.match(target):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
target = urllib.parse.unquote(target)
|
|
33
|
+
|
|
34
|
+
if "\\" in target:
|
|
35
|
+
raise ValueError(f"relationship target is not a POSIX part name: {target!r}")
|
|
36
|
+
|
|
37
|
+
if target.startswith("/"):
|
|
38
|
+
joined = target.lstrip("/")
|
|
39
|
+
else:
|
|
40
|
+
joined = posixpath.join(posixpath.dirname(source_part), target)
|
|
41
|
+
|
|
42
|
+
parts: list[str] = []
|
|
43
|
+
for segment in posixpath.normpath(joined).split("/"):
|
|
44
|
+
if segment in ("", "."):
|
|
45
|
+
continue
|
|
46
|
+
if segment == "..":
|
|
47
|
+
if not parts:
|
|
48
|
+
raise ValueError(f"relationship target escapes the package: {target!r}")
|
|
49
|
+
parts.pop()
|
|
50
|
+
else:
|
|
51
|
+
parts.append(segment)
|
|
52
|
+
|
|
53
|
+
if not parts:
|
|
54
|
+
raise ValueError(f"relationship target resolves to nothing: {target!r}")
|
|
55
|
+
return "/".join(parts)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def rels_source_part(rels_file: Path, unpacked_dir: Path) -> str:
|
|
59
|
+
owner_dir = rels_file.parent.parent.relative_to(unpacked_dir)
|
|
60
|
+
return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]).lstrip("./")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def part_text(data: bytes) -> str:
|
|
64
|
+
return data.decode("utf-8", "surrogateescape")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
XML_SPACE = " \t\r\n"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def rendered_text(text: str, preserve: bool) -> str:
|
|
71
|
+
return text if preserve else text.strip(XML_SPACE)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def safe_extract(zf: zipfile.ZipFile, dest: Path) -> None:
|
|
75
|
+
dest = dest.resolve()
|
|
76
|
+
for m in zf.infolist():
|
|
77
|
+
if stat.S_ISLNK(m.external_attr >> 16):
|
|
78
|
+
raise ValueError(f"symlink archive entry not allowed: {m.filename!r}")
|
|
79
|
+
target = (dest / m.filename).resolve()
|
|
80
|
+
if not target.is_relative_to(dest):
|
|
81
|
+
raise ValueError(f"unsafe archive entry: {m.filename!r}")
|
|
82
|
+
zf.extract(m, dest)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rezip(src_dir: Path, out_path: Path) -> None:
|
|
86
|
+
files = sorted(p for p in src_dir.rglob("*") if p.is_file())
|
|
87
|
+
ct = src_dir / "[Content_Types].xml"
|
|
88
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
89
|
+
prefix=out_path.name + ".", suffix=".tmp", dir=out_path.parent
|
|
90
|
+
)
|
|
91
|
+
tmp_out = Path(tmp_name)
|
|
92
|
+
try:
|
|
93
|
+
with os.fdopen(fd, "wb") as fh:
|
|
94
|
+
with zipfile.ZipFile(fh, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
95
|
+
if ct.exists():
|
|
96
|
+
zf.write(ct, ct.relative_to(src_dir), compress_type=zipfile.ZIP_STORED)
|
|
97
|
+
for f in files:
|
|
98
|
+
if f == ct:
|
|
99
|
+
continue
|
|
100
|
+
zf.write(f, f.relative_to(src_dir))
|
|
101
|
+
if out_path.exists():
|
|
102
|
+
mode = out_path.stat().st_mode & 0o777
|
|
103
|
+
else:
|
|
104
|
+
umask = os.umask(0)
|
|
105
|
+
os.umask(umask)
|
|
106
|
+
mode = 0o666 & ~umask
|
|
107
|
+
os.chmod(tmp_out, mode)
|
|
108
|
+
os.replace(tmp_out, out_path)
|
|
109
|
+
finally:
|
|
110
|
+
if tmp_out.exists():
|
|
111
|
+
tmp_out.unlink()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Find chart XML that PowerPoint refuses but the schema accepts.
|
|
2
|
+
|
|
3
|
+
Detection only: for either fault more than one repair is valid, and only the
|
|
4
|
+
author knows which was meant.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Mapping
|
|
12
|
+
|
|
13
|
+
from . import part_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_CHART_PART_RE = re.compile(r"ppt/charts/chart\d+\.xml")
|
|
17
|
+
|
|
18
|
+
_GROUPING_RE = re.compile(r"""<c:grouping\b[^>]*?\bval=["'](\w+)["']""")
|
|
19
|
+
_DLBL_POS_RE = re.compile(r"""<c:dLblPos\b[^>]*?\bval=["'](\w+)["']""")
|
|
20
|
+
|
|
21
|
+
def _strip_ext_lst(text: str) -> str:
|
|
22
|
+
out, cursor = [], 0
|
|
23
|
+
for lo, hi in _ext_lst_spans(text):
|
|
24
|
+
out.append(text[cursor:lo])
|
|
25
|
+
cursor = hi
|
|
26
|
+
out.append(text[cursor:])
|
|
27
|
+
return "".join(out)
|
|
28
|
+
|
|
29
|
+
_BAR_GROUP_RE = re.compile(r"<c:(bar3DChart|barChart)\b[^>]*(?<!/)>.*?</c:\1\s*>", re.DOTALL)
|
|
30
|
+
|
|
31
|
+
STACKED_GROUPINGS = frozenset({"stacked", "percentStacked"})
|
|
32
|
+
ILLEGAL_ON_STACKED = frozenset({"outEnd"})
|
|
33
|
+
LEGAL_ON_STACKED = ("ctr", "inEnd", "inBase")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _check_stacked_label_positions(part: str, xml: str) -> list[str]:
|
|
37
|
+
problems: list[str] = []
|
|
38
|
+
for match in _BAR_GROUP_RE.finditer(xml):
|
|
39
|
+
block = _strip_ext_lst(match.group(0))
|
|
40
|
+
group = match.group(1)
|
|
41
|
+
|
|
42
|
+
grouping = _GROUPING_RE.search(block)
|
|
43
|
+
if grouping is None or grouping.group(1) not in STACKED_GROUPINGS:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
bad = [p for p in _DLBL_POS_RE.findall(block) if p in ILLEGAL_ON_STACKED]
|
|
47
|
+
for pos in sorted(set(bad)):
|
|
48
|
+
problems.append(
|
|
49
|
+
f'{part}: {bad.count(pos)} data label(s) use dLblPos="{pos}" on a '
|
|
50
|
+
f"{grouping.group(1)} {group}; PowerPoint allows only "
|
|
51
|
+
f"{', '.join(LEGAL_ON_STACKED)} there"
|
|
52
|
+
)
|
|
53
|
+
return problems
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_ANY_CHART_GROUP_RE = re.compile(r"<c:(\w+Chart)\b[^>]*(?<!/)>.*?</c:\1\s*>", re.DOTALL)
|
|
58
|
+
|
|
59
|
+
_AXID_RE = re.compile(
|
|
60
|
+
r"""\s*<c:axId\b[^>]*?\bval=["'](-?\d+)["']\s*(?:/>|>\s*</c:axId\s*>)"""
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_AXIS_DECL_RE = re.compile(
|
|
64
|
+
r"""<c:(catAx|valAx|serAx|dateAx)\b[^>]*(?<!/)>\s*<c:axId\b[^>]*?\bval=["'](-?\d+)["']"""
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
AXID_LIMIT = {
|
|
68
|
+
"barChart": 2, "lineChart": 2, "areaChart": 2, "scatterChart": 2,
|
|
69
|
+
"bubbleChart": 2, "radarChart": 2, "stockChart": 2,
|
|
70
|
+
"bar3DChart": 3, "line3DChart": 3, "area3DChart": 3,
|
|
71
|
+
"surfaceChart": 3, "surface3DChart": 3,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
AXID_MINIMUM = {
|
|
75
|
+
"barChart": 2, "lineChart": 2, "areaChart": 2, "scatterChart": 2,
|
|
76
|
+
"bubbleChart": 2, "radarChart": 2, "stockChart": 2,
|
|
77
|
+
"bar3DChart": 2, "area3DChart": 2, "surfaceChart": 2,
|
|
78
|
+
"line3DChart": 3, "surface3DChart": 3,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _declared_axes(xml: str) -> dict[str, list[str]]:
|
|
83
|
+
axes: dict[str, list[str]] = {}
|
|
84
|
+
for kind, axid in _AXIS_DECL_RE.findall(xml):
|
|
85
|
+
axes.setdefault(kind, []).append(axid)
|
|
86
|
+
return axes
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _canonical_ids(axes: dict[str, list[str]], limit: int) -> list[str] | None:
|
|
90
|
+
category = axes.get("catAx", []) + axes.get("dateAx", [])
|
|
91
|
+
value = axes.get("valAx", [])
|
|
92
|
+
series = axes.get("serAx", [])
|
|
93
|
+
if len(category) != 1 or len(value) != 1 or len(series) > 1:
|
|
94
|
+
return None
|
|
95
|
+
ids = [category[0], value[0]]
|
|
96
|
+
if limit >= 3 and series:
|
|
97
|
+
ids.append(series[0])
|
|
98
|
+
return ids
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _undeclared_axes(kind: str, block: str, axes: dict[str, list[str]]) -> list[str] | None:
|
|
102
|
+
if kind not in AXID_LIMIT:
|
|
103
|
+
return None
|
|
104
|
+
ids = _AXID_RE.findall(block)
|
|
105
|
+
declared = {i for group in axes.values() for i in group}
|
|
106
|
+
if len([i for i in ids if i in declared]) >= 2:
|
|
107
|
+
return None
|
|
108
|
+
return ids
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check_chart_axis_references(part: str, xml: str) -> list[str]:
|
|
112
|
+
axes = _declared_axes(xml)
|
|
113
|
+
problems: list[str] = []
|
|
114
|
+
declared = {i for group in axes.values() for i in group}
|
|
115
|
+
for match in _ANY_CHART_GROUP_RE.finditer(xml):
|
|
116
|
+
kind, block = match.group(1), match.group(0)
|
|
117
|
+
ids = _undeclared_axes(kind, block, axes)
|
|
118
|
+
if ids is None:
|
|
119
|
+
continue
|
|
120
|
+
if not ids:
|
|
121
|
+
problems.append(
|
|
122
|
+
f"{part}: <c:{kind}> declares no <c:axId> this part can resolve; a chart "
|
|
123
|
+
f"group needs {AXID_MINIMUM[kind]}, and PowerPoint discards one with fewer"
|
|
124
|
+
)
|
|
125
|
+
continue
|
|
126
|
+
dead = [i for i in ids if i not in declared]
|
|
127
|
+
canonical = _canonical_ids(axes, AXID_LIMIT[kind])
|
|
128
|
+
if canonical is not None and len(canonical) >= AXID_MINIMUM[kind]:
|
|
129
|
+
hint = f"Fix: point them at the axes this part declares ({', '.join(canonical)})"
|
|
130
|
+
else:
|
|
131
|
+
hint = ("Fix: the part declares several axes of a kind -- declare the "
|
|
132
|
+
"secondary axes the series expects, or drop them")
|
|
133
|
+
detail = (f"of which {', '.join(dead)} name no declared axis"
|
|
134
|
+
if dead else f"only {len(ids)} of which this part declares")
|
|
135
|
+
problems.append(
|
|
136
|
+
f"{part}: <c:{kind}> references axId {', '.join(ids)}, {detail}, "
|
|
137
|
+
f"leaving fewer than two live axes; PowerPoint discards the chart. {hint}"
|
|
138
|
+
)
|
|
139
|
+
return problems
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _ext_lst_spans(text: str) -> list[tuple[int, int]]:
|
|
143
|
+
spans: list[tuple[int, int]] = []
|
|
144
|
+
depth = 0
|
|
145
|
+
start = 0
|
|
146
|
+
for match in re.finditer(r"<(/?)c:extLst\b[^>]*?(/?)>", text):
|
|
147
|
+
closing, self_closing = match.group(1), match.group(2)
|
|
148
|
+
if self_closing:
|
|
149
|
+
continue
|
|
150
|
+
if closing:
|
|
151
|
+
depth -= 1
|
|
152
|
+
if depth == 0:
|
|
153
|
+
spans.append((start, match.end()))
|
|
154
|
+
else:
|
|
155
|
+
if depth == 0:
|
|
156
|
+
start = match.start()
|
|
157
|
+
depth += 1
|
|
158
|
+
return spans
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
CHART_CHECKS = (_check_stacked_label_positions, _check_chart_axis_references)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def find_chart_problems(files: Mapping[str, bytes]) -> list[str]:
|
|
165
|
+
problems: list[str] = []
|
|
166
|
+
for part in sorted(n for n in files if _CHART_PART_RE.fullmatch(n)):
|
|
167
|
+
xml = part_text(files[part])
|
|
168
|
+
for check in CHART_CHECKS:
|
|
169
|
+
problems.extend(check(part, xml))
|
|
170
|
+
return problems
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Pick the slide-XML schema errors PowerPoint refuses the file over.
|
|
2
|
+
|
|
3
|
+
A denylist over lxml's messages, so an unrecognised error class is a miss rather
|
|
4
|
+
than a false alarm.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
SLIDE_PART_RE = re.compile(
|
|
13
|
+
r"ppt/(slides|slideLayouts|slideMasters|notesSlides|notesMasters|handoutMasters)"
|
|
14
|
+
r"/[^/]+\.xml"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
FATAL_SLIDE_ERRORS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
18
|
+
(
|
|
19
|
+
re.compile(r"\}tableStyleId': This element is not expected"),
|
|
20
|
+
"two <a:tableStyleId> in one <a:tblPr> (the schema allows one)",
|
|
21
|
+
),
|
|
22
|
+
(
|
|
23
|
+
re.compile(r"\}srgbClr', attribute 'val'"),
|
|
24
|
+
"a colour that is not six hex digits",
|
|
25
|
+
),
|
|
26
|
+
(
|
|
27
|
+
re.compile(r"\}txBody': Missing child element"),
|
|
28
|
+
"a <p:txBody> with no children",
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
re.compile(r"\}miter', attribute 'lim'"),
|
|
32
|
+
'a line join with lim="NaN"',
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
re.compile(r"\}uLnTx': This element is not expected"),
|
|
36
|
+
"<a:uLnTx> in a position the schema forbids",
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
re.compile(r"\}overrideClrMapping': This element is not expected"),
|
|
40
|
+
"<p:overrideClrMapping> in a position the schema forbids",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
re.compile(r"\}nvGrpSpPr': Missing child element"),
|
|
44
|
+
"a <p:nvGrpSpPr> with no children",
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_schema_verdict(error: str) -> bool:
|
|
50
|
+
return error.startswith("Element ")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fatal_slide_errors(errors: set[str]) -> list[str]:
|
|
54
|
+
out = []
|
|
55
|
+
for error in sorted(errors):
|
|
56
|
+
for pattern, meaning in FATAL_SLIDE_ERRORS:
|
|
57
|
+
if pattern.search(error):
|
|
58
|
+
out.append(f"{meaning}: {error}")
|
|
59
|
+
break
|
|
60
|
+
return out
|