@huicai/cli 2026.804.0 → 2026.1019.0
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/LICENSE +391 -32
- package/README.md +176 -10
- package/bin/huicai.js +166 -73
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -0
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/errkind.js +1 -0
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -0
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -0
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
|
@@ -1,161 +1,21 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import re
|
|
1
|
+
# 兼容垫片:老入口 md_to_docx() / verify() 保留,实现已迁到 docx_build.py + docx_check.py。
|
|
2
|
+
#
|
|
3
|
+
# 为什么保留而不是直接删:外部脚本与历史会话里可能还 read_skill_file 到这个文件名。
|
|
4
|
+
# 新代码请直接用 create_doc 工具,或 import docx_build。
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
from docx.oxml.ns import qn
|
|
12
|
-
from docx.oxml import OxmlElement
|
|
13
|
-
from docx.shared import Pt
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _set_ea(style, west, east, size=None, bold=None):
|
|
17
|
-
"""中文字体必须同时设 w:eastAsia,只设 font.name 中文不生效。"""
|
|
18
|
-
style.font.name = west
|
|
19
|
-
style.element.rPr.rFonts.set(qn('w:eastAsia'), east)
|
|
20
|
-
if size is not None:
|
|
21
|
-
style.font.size = Pt(size)
|
|
22
|
-
if bold is not None:
|
|
23
|
-
style.font.bold = bold
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _field(run, instr, placeholder=''):
|
|
27
|
-
"""在 run 上插入 Word 域(TOC/PAGE/NUMPAGES…)。"""
|
|
28
|
-
begin = OxmlElement('w:fldChar')
|
|
29
|
-
begin.set(qn('w:fldCharType'), 'begin')
|
|
30
|
-
it = OxmlElement('w:instrText')
|
|
31
|
-
it.set(qn('xml:space'), 'preserve')
|
|
32
|
-
it.text = instr
|
|
33
|
-
sep = OxmlElement('w:fldChar')
|
|
34
|
-
sep.set(qn('w:fldCharType'), 'separate')
|
|
35
|
-
t = OxmlElement('w:t')
|
|
36
|
-
t.text = placeholder
|
|
37
|
-
end = OxmlElement('w:fldChar')
|
|
38
|
-
end.set(qn('w:fldCharType'), 'end')
|
|
39
|
-
for el in (begin, it, sep, t, end):
|
|
40
|
-
run._r.append(el)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def new_doc():
|
|
44
|
-
"""建空文档并配好中英文字体样式。"""
|
|
45
|
-
doc = Document()
|
|
46
|
-
_set_ea(doc.styles['Normal'], 'Calibri', '宋体', size=11)
|
|
47
|
-
for lvl, size in (('Heading 1', 16), ('Heading 2', 14), ('Heading 3', 12)):
|
|
48
|
-
_set_ea(doc.styles[lvl], 'Calibri', '黑体', size=size, bold=True)
|
|
49
|
-
return doc
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def add_cover(doc, title, author='', subtitle=''):
|
|
53
|
-
for _ in range(6):
|
|
54
|
-
doc.add_paragraph()
|
|
55
|
-
p = doc.add_paragraph()
|
|
56
|
-
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
57
|
-
r = p.add_run(title)
|
|
58
|
-
r.font.size = Pt(28)
|
|
59
|
-
r.font.bold = True
|
|
60
|
-
r.font.name = 'Calibri'
|
|
61
|
-
r._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
|
|
62
|
-
if subtitle:
|
|
63
|
-
p2 = doc.add_paragraph()
|
|
64
|
-
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
65
|
-
p2.add_run(subtitle).font.size = Pt(14)
|
|
66
|
-
meta = doc.add_paragraph()
|
|
67
|
-
meta.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
68
|
-
meta.add_run(f"{author + ' ' if author else ''}{datetime.date.today().strftime('%Y年%m月%d日')}").font.size = Pt(12)
|
|
69
|
-
doc.add_page_break()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def add_toc(doc):
|
|
73
|
-
doc.add_paragraph('目录', style='Heading 1')
|
|
74
|
-
_field(doc.add_paragraph().add_run(), 'TOC \\o "1-3" \\h \\z \\u', '(在 Word 中右键→更新域,或按 F9 生成目录)')
|
|
75
|
-
doc.add_page_break()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def add_page_footer(doc):
|
|
79
|
-
p = doc.sections[0].footer.paragraphs[0]
|
|
80
|
-
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
81
|
-
p.add_run('第 ')
|
|
82
|
-
_field(p.add_run(), 'PAGE', '1')
|
|
83
|
-
p.add_run(' 页 共 ')
|
|
84
|
-
_field(p.add_run(), 'NUMPAGES', '1')
|
|
85
|
-
p.add_run(' 页')
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _add_runs(par, text):
|
|
89
|
-
"""按 **粗体** 拆 run。"""
|
|
90
|
-
for i, seg in enumerate(re.split(r'\*\*(.+?)\*\*', text)):
|
|
91
|
-
if seg:
|
|
92
|
-
par.add_run(seg).bold = (i % 2 == 1)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _flush_table(doc, rows):
|
|
96
|
-
cells = [[c.strip() for c in r.strip().strip('|').split('|')] for r in rows]
|
|
97
|
-
cells = [r for r in cells if not all(re.fullmatch(r':?-{2,}:?', c or '---') for c in r)] # 去分隔行
|
|
98
|
-
if not cells:
|
|
99
|
-
return
|
|
100
|
-
tb = doc.add_table(rows=len(cells), cols=len(cells[0]))
|
|
101
|
-
tb.style = 'Table Grid'
|
|
102
|
-
for i, row in enumerate(cells):
|
|
103
|
-
for j, val in enumerate(row[:len(tb.columns)]):
|
|
104
|
-
par = tb.cell(i, j).paragraphs[0]
|
|
105
|
-
_add_runs(par, val)
|
|
106
|
-
if i == 0:
|
|
107
|
-
for r in par.runs:
|
|
108
|
-
r.bold = True
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def md_to_docx(md_path, out_path, title='', author='', subtitle='', toc=True):
|
|
112
|
-
"""Markdown → Word。返回 out_path。"""
|
|
113
|
-
doc = new_doc()
|
|
114
|
-
add_page_footer(doc)
|
|
115
|
-
if title:
|
|
116
|
-
add_cover(doc, title, author, subtitle)
|
|
117
|
-
if toc:
|
|
118
|
-
add_toc(doc)
|
|
119
|
-
lines = open(md_path, encoding='utf-8').read().splitlines()
|
|
120
|
-
table = []
|
|
121
|
-
for line in lines + ['']: # 末尾哨兵,冲刷表格
|
|
122
|
-
s = line.rstrip()
|
|
123
|
-
if s.lstrip().startswith('|'):
|
|
124
|
-
table.append(s)
|
|
125
|
-
continue
|
|
126
|
-
if table:
|
|
127
|
-
_flush_table(doc, table)
|
|
128
|
-
table = []
|
|
129
|
-
if not s.strip():
|
|
130
|
-
continue
|
|
131
|
-
m = re.match(r'^(#{1,3})\s+(.*)', s)
|
|
132
|
-
if m:
|
|
133
|
-
doc.add_paragraph(m.group(2).strip(), style=f'Heading {len(m.group(1))}')
|
|
134
|
-
elif re.match(r'^\s*-\s+', s):
|
|
135
|
-
_add_runs(doc.add_paragraph(style='List Bullet'), re.sub(r'^\s*-\s+', '', s))
|
|
136
|
-
elif re.match(r'^\s*\d+[.、]\s*', s):
|
|
137
|
-
_add_runs(doc.add_paragraph(style='List Number'), re.sub(r'^\s*\d+[.、]\s*', '', s))
|
|
138
|
-
else:
|
|
139
|
-
_add_runs(doc.add_paragraph(), s)
|
|
140
|
-
doc.save(out_path)
|
|
141
|
-
return out_path
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
+
from docx_build import DocBuilder, build, md_to_blocks, md_to_docx # noqa: F401,E402
|
|
142
10
|
|
|
143
11
|
|
|
144
12
|
def verify(path):
|
|
145
|
-
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
print(
|
|
149
|
-
|
|
150
|
-
print(' ' * (int(style[-1]) - 1) + f'- [{style}] {text}')
|
|
151
|
-
return heads
|
|
13
|
+
"""重读校验(老接口)。新代码用 docx_check.inspect() —— 它查的远不止标题树。"""
|
|
14
|
+
import docx_check
|
|
15
|
+
res = docx_check.inspect(path)
|
|
16
|
+
print(docx_check.report(res))
|
|
17
|
+
return res
|
|
152
18
|
|
|
153
19
|
|
|
154
20
|
if __name__ == '__main__':
|
|
155
|
-
|
|
156
|
-
open(md, 'w', encoding='utf-8').write(
|
|
157
|
-
'# 一、背景\n项目**按期**推进。\n\n## 1.1 目标\n- 目标甲\n- 目标乙\n\n'
|
|
158
|
-
'# 二、数据\n| 地区 | 销售额 |\n|---|---|\n| 华东 | 1200 |\n| 华南 | 950 |\n\n1. 结论一\n2. 结论二\n')
|
|
159
|
-
p = md_to_docx(md, 'docx_doc_check.docx', title='自测报告', author='Work Twin', subtitle='docx_doc.py 冒烟')
|
|
160
|
-
verify(p)
|
|
161
|
-
print('OK', p)
|
|
21
|
+
print(__doc__)
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# Word 技术文档排版度量与规范内核(docx_build 建版 / docx_check 体检 共用同一套度量)。
|
|
2
|
+
#
|
|
3
|
+
# 为什么必须共用:建版时算「这张表放不放得下版心」和体检时算「这张表有没有超出版心」,
|
|
4
|
+
# 只要用两套度量,就一定会出现「生成端说没事、体检端说超宽」的自相矛盾。
|
|
5
|
+
# 一份度量、两个方向用——这是本模块存在的唯一理由(与 pptx_layout 同一条纪律)。
|
|
6
|
+
#
|
|
7
|
+
# 度量口径(Word 渲染,中文宋体/黑体 + 西文 Times New Roman):
|
|
8
|
+
# · 字宽按 em 表逐字累加(中日韩全角 1.0em,Times 数字 0.5em,小写 0.50em…);
|
|
9
|
+
# · 单倍行高 1.32×字号(SimSun ascent+descent≈1.32em);
|
|
10
|
+
# · 版心宽 = 纸宽 − 左右页边距,**所有「会不会超宽」的判断都以它为准**。
|
|
11
|
+
# 估算不可能与 Word 逐像素一致,所以留了 TOL 容差,且报告一律带出
|
|
12
|
+
# 「估算值 vs 可用值」的具体数字,让人能自己判断,而不是只丢一个结论。
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
# ── 单位 ───────────────────────────────────────────────────────────────────
|
|
17
|
+
CM_PT = 28.3464567 # 1 厘米 = 28.35 磅
|
|
18
|
+
PT_CM = 1.0 / CM_PT
|
|
19
|
+
EMU_PT = 12700 # 1 磅 = 12700 EMU
|
|
20
|
+
EMU_CM = 360000 # 1 厘米 = 360000 EMU
|
|
21
|
+
PT_IN = 72.0
|
|
22
|
+
|
|
23
|
+
LINE_H = 1.32 # 单倍行距 ≈ 1.32 × 字号(宋体 ascent 1060 + descent 380 / 1000 ≈ 1.44 排版实测取 1.32)
|
|
24
|
+
WRAP_SAFETY = 0.98 # 折行留 2% 余量:字宽估窄一点就会少算一行,少算一行就是「说放得下、实际挤出去」
|
|
25
|
+
TOL = 1.02 # 超限容差:估算超出可用空间 2% 以内不报(度量误差带)
|
|
26
|
+
|
|
27
|
+
# ── 中文字号 → 磅 ──────────────────────────────────────────────────────────
|
|
28
|
+
SIZE_PT = {
|
|
29
|
+
'初号': 42, '小初': 36, '一号': 26, '小一': 24, '二号': 22, '小二': 18,
|
|
30
|
+
'三号': 16, '小三': 15, '四号': 14, '小四': 12, '五号': 10.5, '小五': 9,
|
|
31
|
+
'六号': 7.5, '小六': 6.5, '七号': 5.5, '八号': 5,
|
|
32
|
+
}
|
|
33
|
+
PT_SIZE = {v: k for k, v in SIZE_PT.items()}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def pt_of(size):
|
|
37
|
+
"""'小四' / 12 / '12' → 12.0 磅。"""
|
|
38
|
+
if isinstance(size, str):
|
|
39
|
+
s = size.strip()
|
|
40
|
+
if s in SIZE_PT:
|
|
41
|
+
return float(SIZE_PT[s])
|
|
42
|
+
try:
|
|
43
|
+
return float(s)
|
|
44
|
+
except ValueError:
|
|
45
|
+
return 12.0
|
|
46
|
+
return float(size)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def size_name(pt):
|
|
50
|
+
"""12.0 → '小四';非标准字号返回 '12pt'(体检报告里要说人话)。"""
|
|
51
|
+
for v, n in PT_SIZE.items():
|
|
52
|
+
if abs(v - pt) < 0.26:
|
|
53
|
+
return n
|
|
54
|
+
return f'{pt:g}pt'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── 字宽表(em)────────────────────────────────────────────────────────────
|
|
58
|
+
# 取值不是拍脑袋:中文按全角 1.0em(宋体/黑体/仿宋均为等宽全角,这一条是确定的);
|
|
59
|
+
# 西文按 Times New Roman(Word 中文文档的默认西文字体,unitsPerEm 2048)实测 advance /2048,
|
|
60
|
+
# 并统一向上取一档——**宁可估宽,不可估窄**:估宽了最多是提前折一行留白多一点,
|
|
61
|
+
# 估窄了就是「说这列放得下、实际把表挤出版心」,那才是要命的。
|
|
62
|
+
W_CJK = 1.0
|
|
63
|
+
W_DIGIT = 0.52 # Times 数字 1024/2048 = 0.500
|
|
64
|
+
W_UPPER = 0.73 # Times 'A' 0.72 / 'M' 0.89 / 'I' 0.33,取常见上沿
|
|
65
|
+
W_LOWER = 0.50 # Times 'a' 0.44 / 'm' 0.78 / 'i' 0.28
|
|
66
|
+
W_SPACE = 0.27 # Times space 512/2048 = 0.250
|
|
67
|
+
W_NARROW = 0.28
|
|
68
|
+
W_WIDE = 0.88
|
|
69
|
+
|
|
70
|
+
NARROW = set(".,;:!|'`iIljtf()[]{}-·")
|
|
71
|
+
WIDE = set("@%&WMmw#—…")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _full_width(cp):
|
|
75
|
+
"""全角判定:CJK 汉字、假名、谚文、全角标点、CJK 兼容形。"""
|
|
76
|
+
return (
|
|
77
|
+
0x1100 <= cp <= 0x115F or 0x2E80 <= cp <= 0xA4CF or 0xAC00 <= cp <= 0xD7A3
|
|
78
|
+
or 0xF900 <= cp <= 0xFAFF or 0xFE10 <= cp <= 0xFE6F or 0xFF00 <= cp <= 0xFF60
|
|
79
|
+
or 0xFFE0 <= cp <= 0xFFE6 or 0x3000 <= cp <= 0x303F
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def char_em(ch):
|
|
84
|
+
"""单字符宽度(em)。"""
|
|
85
|
+
cp = ord(ch)
|
|
86
|
+
if ch == ' ':
|
|
87
|
+
return W_SPACE
|
|
88
|
+
if ch == '\t':
|
|
89
|
+
return W_SPACE * 4
|
|
90
|
+
if _full_width(cp):
|
|
91
|
+
return W_CJK
|
|
92
|
+
if cp > 0xFFFF:
|
|
93
|
+
return 1.15
|
|
94
|
+
if ch in NARROW:
|
|
95
|
+
return W_NARROW
|
|
96
|
+
if ch in WIDE:
|
|
97
|
+
return W_WIDE
|
|
98
|
+
if '0' <= ch <= '9':
|
|
99
|
+
return W_DIGIT
|
|
100
|
+
if 'A' <= ch <= 'Z':
|
|
101
|
+
return W_UPPER
|
|
102
|
+
if 'a' <= ch <= 'z':
|
|
103
|
+
return W_LOWER
|
|
104
|
+
if cp < 0x2E80:
|
|
105
|
+
return W_LOWER
|
|
106
|
+
return W_CJK
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def measure(text, size, bold=False):
|
|
110
|
+
"""估算一段文本的渲染宽度(磅)。size 可传 '小四' 或 12。"""
|
|
111
|
+
if not text:
|
|
112
|
+
return 0.0
|
|
113
|
+
pt = pt_of(size)
|
|
114
|
+
w = sum(char_em(ch) for ch in str(text)) * pt
|
|
115
|
+
return w * (1.06 if bold else 1.0) # 加粗按 6% 加宽(宋体加粗是合成粗体,会变宽)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def wrap_count(text, size, width_pt, first_indent_pt=0.0):
|
|
119
|
+
"""这段文字在给定宽度里要占几行(首行可另设缩进)。空文本算 1 行——Word 里空段也占一行。"""
|
|
120
|
+
s = str(text or '')
|
|
121
|
+
if not s.strip():
|
|
122
|
+
return 1
|
|
123
|
+
pt = pt_of(size)
|
|
124
|
+
avail_first = max(width_pt - first_indent_pt, pt)
|
|
125
|
+
avail = max(width_pt, pt)
|
|
126
|
+
lines, cur, limit = 1, 0.0, avail_first * WRAP_SAFETY
|
|
127
|
+
for ch in s:
|
|
128
|
+
if ch == '\n':
|
|
129
|
+
lines += 1
|
|
130
|
+
cur, limit = 0.0, avail * WRAP_SAFETY
|
|
131
|
+
continue
|
|
132
|
+
w = char_em(ch) * pt
|
|
133
|
+
if cur + w > limit:
|
|
134
|
+
lines += 1
|
|
135
|
+
cur, limit = w, avail * WRAP_SAFETY
|
|
136
|
+
else:
|
|
137
|
+
cur += w
|
|
138
|
+
return lines
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── 排版规范档位 ───────────────────────────────────────────────────────────
|
|
142
|
+
# 三档规范都是「有出处的」,不是审美偏好:
|
|
143
|
+
# tech —— 企业技术/项目管理文档通行做法(A4、左右 3.17cm、正文宋体小四、1.5 倍行距、首行缩进 2 字符);
|
|
144
|
+
# gov —— GB/T 9704《党政机关公文格式》:正文仿宋_GB2312 三号,每面 22 行、每行 28 字,
|
|
145
|
+
# 一级「一、」黑体三号、二级「(一)」楷体三号、三级「1.」仿宋三号加粗;
|
|
146
|
+
# report —— 科技报告/学术稿:小四宋体、固定 20 磅行距、十进制编号 1 / 1.1 / 1.1.1。
|
|
147
|
+
# 改这里=改全局排版,建版与体检同时跟着变,不会各改各的。
|
|
148
|
+
|
|
149
|
+
_BASE = {
|
|
150
|
+
'page': 'A4',
|
|
151
|
+
'page_w_cm': 21.0,
|
|
152
|
+
'page_h_cm': 29.7,
|
|
153
|
+
'margin_cm': {'top': 2.54, 'bottom': 2.54, 'left': 3.17, 'right': 3.17},
|
|
154
|
+
'body': {'east': '宋体', 'west': 'Times New Roman', 'size': '小四',
|
|
155
|
+
'line_rule': 'multiple', 'line': 1.5, 'first_indent_chars': 2,
|
|
156
|
+
'space_before': 0, 'space_after': 0, 'align': 'justify'},
|
|
157
|
+
'headings': [
|
|
158
|
+
{'east': '黑体', 'west': 'Times New Roman', 'size': '三号', 'bold': False,
|
|
159
|
+
'align': 'center', 'space_before': 18, 'space_after': 12, 'line': 1.5},
|
|
160
|
+
{'east': '黑体', 'west': 'Times New Roman', 'size': '四号', 'bold': False,
|
|
161
|
+
'align': 'left', 'space_before': 12, 'space_after': 6, 'line': 1.5},
|
|
162
|
+
{'east': '黑体', 'west': 'Times New Roman', 'size': '小四', 'bold': False,
|
|
163
|
+
'align': 'left', 'space_before': 6, 'space_after': 6, 'line': 1.5},
|
|
164
|
+
{'east': '楷体', 'west': 'Times New Roman', 'size': '小四', 'bold': True,
|
|
165
|
+
'align': 'left', 'space_before': 6, 'space_after': 0, 'line': 1.5},
|
|
166
|
+
],
|
|
167
|
+
'caption': {'east': '黑体', 'west': 'Times New Roman', 'size': '五号',
|
|
168
|
+
'align': 'center', 'line': 1.0},
|
|
169
|
+
'table': {'east': '宋体', 'west': 'Times New Roman', 'size': '五号',
|
|
170
|
+
'head_bold': True, 'align': 'center', 'line': 1.0, 'style': '三线表'},
|
|
171
|
+
'numbering': 'cn', # cn = 一、/(一)/1./(1);decimal = 1/1.1/1.1.1;none = 不编号
|
|
172
|
+
'fig_prefix': '图',
|
|
173
|
+
'tab_prefix': '表',
|
|
174
|
+
'caption_by_chapter': False, # True = 图 1-1(按章编号),False = 图 1(全文连续)
|
|
175
|
+
'toc_levels': 3,
|
|
176
|
+
'max_image_cm': None, # None = 版心宽
|
|
177
|
+
'name': 'tech',
|
|
178
|
+
'label': '技术文档(A4/宋体小四/1.5 倍行距/首行缩进 2 字符)',
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _merge(base, over):
|
|
183
|
+
out = {}
|
|
184
|
+
for k, v in base.items():
|
|
185
|
+
if isinstance(v, dict) and isinstance(over.get(k), dict):
|
|
186
|
+
out[k] = _merge(v, over[k])
|
|
187
|
+
elif isinstance(v, list) and isinstance(over.get(k), list):
|
|
188
|
+
out[k] = [_merge(a, b) if isinstance(a, dict) and isinstance(b, dict) else b
|
|
189
|
+
for a, b in zip(v, over[k])] + over[k][len(v):]
|
|
190
|
+
else:
|
|
191
|
+
out[k] = over[k] if k in over else v
|
|
192
|
+
for k, v in over.items():
|
|
193
|
+
if k not in out:
|
|
194
|
+
out[k] = v
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
_OVERRIDES = {
|
|
199
|
+
'tech': {},
|
|
200
|
+
'gov': {
|
|
201
|
+
'name': 'gov',
|
|
202
|
+
'label': '党政机关公文 GB/T 9704(仿宋三号/固定 28.8 磅行距/每面 22 行)',
|
|
203
|
+
'margin_cm': {'top': 3.7, 'bottom': 3.5, 'left': 2.8, 'right': 2.6},
|
|
204
|
+
'body': {'east': '仿宋_GB2312', 'west': 'Times New Roman', 'size': '三号',
|
|
205
|
+
'line_rule': 'exact', 'line': 28.8, 'first_indent_chars': 2,
|
|
206
|
+
'space_before': 0, 'space_after': 0, 'align': 'justify'},
|
|
207
|
+
'headings': [
|
|
208
|
+
{'east': '黑体', 'size': '三号', 'bold': False, 'align': 'left',
|
|
209
|
+
'space_before': 0, 'space_after': 0, 'line': 28.8, 'line_rule': 'exact'},
|
|
210
|
+
{'east': '楷体_GB2312', 'size': '三号', 'bold': False, 'align': 'left',
|
|
211
|
+
'space_before': 0, 'space_after': 0, 'line': 28.8, 'line_rule': 'exact'},
|
|
212
|
+
{'east': '仿宋_GB2312', 'size': '三号', 'bold': True, 'align': 'left',
|
|
213
|
+
'space_before': 0, 'space_after': 0, 'line': 28.8, 'line_rule': 'exact'},
|
|
214
|
+
{'east': '仿宋_GB2312', 'size': '三号', 'bold': True, 'align': 'left',
|
|
215
|
+
'space_before': 0, 'space_after': 0, 'line': 28.8, 'line_rule': 'exact'},
|
|
216
|
+
],
|
|
217
|
+
'table': {'east': '仿宋_GB2312', 'size': '小四', 'head_bold': True},
|
|
218
|
+
'caption': {'east': '黑体', 'size': '五号'},
|
|
219
|
+
'numbering': 'cn',
|
|
220
|
+
},
|
|
221
|
+
'report': {
|
|
222
|
+
'name': 'report',
|
|
223
|
+
'label': '科技报告/学术(宋体小四/固定 20 磅行距/十进制编号)',
|
|
224
|
+
'margin_cm': {'top': 2.54, 'bottom': 2.54, 'left': 3.0, 'right': 3.0},
|
|
225
|
+
'body': {'line_rule': 'exact', 'line': 20, 'first_indent_chars': 2},
|
|
226
|
+
'headings': [
|
|
227
|
+
{'east': '黑体', 'size': '三号', 'align': 'center', 'space_before': 18, 'space_after': 12},
|
|
228
|
+
{'east': '黑体', 'size': '四号', 'align': 'left', 'space_before': 12, 'space_after': 6},
|
|
229
|
+
{'east': '黑体', 'size': '小四', 'align': 'left', 'space_before': 6, 'space_after': 6},
|
|
230
|
+
{'east': '黑体', 'size': '小四', 'bold': False, 'align': 'left', 'space_before': 6, 'space_after': 0},
|
|
231
|
+
],
|
|
232
|
+
'numbering': 'decimal',
|
|
233
|
+
'caption_by_chapter': True,
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
PROFILES = {k: _merge(_BASE, v) for k, v in _OVERRIDES.items()}
|
|
238
|
+
PROFILE_NAMES = list(PROFILES)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def profile(name='tech'):
|
|
242
|
+
"""取一份规范档位(深拷贝,调用方可以就地改而不污染全局)。"""
|
|
243
|
+
p = PROFILES.get(str(name or 'tech').strip(), PROFILES['tech'])
|
|
244
|
+
return _merge(p, {})
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── 版心几何 ───────────────────────────────────────────────────────────────
|
|
248
|
+
def body_width_pt(prof):
|
|
249
|
+
"""版心宽(磅)——所有「会不会超宽」的判断都用它。"""
|
|
250
|
+
m = prof['margin_cm']
|
|
251
|
+
return (prof['page_w_cm'] - m['left'] - m['right']) * CM_PT
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def body_height_pt(prof):
|
|
255
|
+
"""版心高(磅)。页眉页脚在页边距内,这里按正文可用高度算。"""
|
|
256
|
+
m = prof['margin_cm']
|
|
257
|
+
return (prof['page_h_cm'] - m['top'] - m['bottom']) * CM_PT
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def body_width_cm(prof):
|
|
261
|
+
m = prof['margin_cm']
|
|
262
|
+
return prof['page_w_cm'] - m['left'] - m['right']
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def line_height_pt(prof, style=None):
|
|
266
|
+
"""一行占多高(磅)。exact=固定值直接返回;multiple=倍数×单倍行高。"""
|
|
267
|
+
st = style or prof['body']
|
|
268
|
+
size = pt_of(st.get('size', prof['body']['size']))
|
|
269
|
+
rule = st.get('line_rule', prof['body'].get('line_rule', 'multiple'))
|
|
270
|
+
val = st.get('line', prof['body'].get('line', 1.5))
|
|
271
|
+
if rule == 'exact':
|
|
272
|
+
return float(val)
|
|
273
|
+
return size * LINE_H * float(val)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def indent_pt(prof, style=None):
|
|
277
|
+
"""首行缩进(磅)= 缩进字符数 × 字号。中文排版的「2 字符」是随字号变的,不是固定磅值。"""
|
|
278
|
+
st = style or prof['body']
|
|
279
|
+
return float(st.get('first_indent_chars', 0)) * pt_of(st.get('size', prof['body']['size']))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def est_para_height(text, prof, style=None):
|
|
283
|
+
"""一段文字的估算高度(磅),含段前段后。"""
|
|
284
|
+
st = style or prof['body']
|
|
285
|
+
size = st.get('size', prof['body']['size'])
|
|
286
|
+
n = wrap_count(text, size, body_width_pt(prof), indent_pt(prof, st))
|
|
287
|
+
return n * line_height_pt(prof, st) + float(st.get('space_before', 0)) + float(st.get('space_after', 0))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ── 表格列宽(先量后画的核心)───────────────────────────────────────────────
|
|
291
|
+
def fit_table_widths(rows, prof, size=None, min_col_cm=1.2, pad_pt=8.0):
|
|
292
|
+
"""按内容算列宽,总宽压进版心。返回每列宽度(厘米)。
|
|
293
|
+
|
|
294
|
+
做法:先按「每列最长单元格的自然宽度」分配,超版心时按超出比例压缩,
|
|
295
|
+
但每列不低于 min_col_cm(压到比这还窄,字会被逼成竖排,那还不如让它折行)。
|
|
296
|
+
压不下去的(列太多)会把 min 一起等比降,并由调用方决定是否横排页面。
|
|
297
|
+
"""
|
|
298
|
+
if not rows:
|
|
299
|
+
return []
|
|
300
|
+
cols = max(len(r) for r in rows)
|
|
301
|
+
size = size or prof['table']['size']
|
|
302
|
+
avail = body_width_pt(prof)
|
|
303
|
+
nat = []
|
|
304
|
+
for j in range(cols):
|
|
305
|
+
w = 0.0
|
|
306
|
+
for r in rows:
|
|
307
|
+
if j < len(r):
|
|
308
|
+
cell = str(r[j] if r[j] is not None else '')
|
|
309
|
+
w = max(w, max((measure(seg, size) for seg in cell.split('\n')), default=0.0))
|
|
310
|
+
nat.append(min(w + pad_pt, avail * 0.62)) # 单列上限 62% 版心,防一列吃掉整张表
|
|
311
|
+
total = sum(nat) or 1.0
|
|
312
|
+
if total <= avail:
|
|
313
|
+
# 有富余就把剩下的按自然宽比例摊回去,让表格铺满版心(半张表宽的表格很难看)
|
|
314
|
+
extra = avail - total
|
|
315
|
+
nat = [w + extra * (w / total) for w in nat]
|
|
316
|
+
return [round(w * PT_CM, 3) for w in nat]
|
|
317
|
+
floor_pt = min_col_cm * CM_PT
|
|
318
|
+
if floor_pt * cols > avail:
|
|
319
|
+
floor_pt = avail / cols * 0.8
|
|
320
|
+
slack = [max(w - floor_pt, 0.0) for w in nat]
|
|
321
|
+
over, slack_sum = total - avail, sum(slack)
|
|
322
|
+
if slack_sum <= 0:
|
|
323
|
+
return [round(avail / cols * PT_CM, 3)] * cols
|
|
324
|
+
ratio = min(over / slack_sum, 1.0)
|
|
325
|
+
out = [w - s * ratio for w, s in zip(nat, slack)]
|
|
326
|
+
scale = avail / sum(out) if sum(out) > avail else 1.0
|
|
327
|
+
return [round(w * scale * PT_CM, 3) for w in out]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def est_table_height(rows, prof, widths_cm=None, size=None, cell_pad_pt=4.0):
|
|
331
|
+
"""表格估算高度(磅):逐格算折行数取每行最大。用于判断「这张表会不会顶出页底」。"""
|
|
332
|
+
if not rows:
|
|
333
|
+
return 0.0
|
|
334
|
+
size = size or prof['table']['size']
|
|
335
|
+
widths_cm = widths_cm or fit_table_widths(rows, prof, size)
|
|
336
|
+
lh = pt_of(size) * LINE_H
|
|
337
|
+
h = 0.0
|
|
338
|
+
for r in rows:
|
|
339
|
+
rows_lines = 1
|
|
340
|
+
for j, cell in enumerate(r):
|
|
341
|
+
w = (widths_cm[j] if j < len(widths_cm) else widths_cm[-1]) * CM_PT - cell_pad_pt * 2
|
|
342
|
+
rows_lines = max(rows_lines, wrap_count(str(cell or ''), size, max(w, pt_of(size))))
|
|
343
|
+
h += rows_lines * lh + cell_pad_pt * 2
|
|
344
|
+
return h
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ── 图片 ───────────────────────────────────────────────────────────────────
|
|
348
|
+
def image_box(px_w, px_h, prof, max_w_cm=None, max_h_cm=None, target_dpi=150):
|
|
349
|
+
"""按原比例算插图显示尺寸(厘米):先按 target_dpi 折算自然尺寸,超版心/超高再等比缩。
|
|
350
|
+
|
|
351
|
+
**不存在把图拉变形的写法**——宽高永远同一个 scale,这与 pptx 侧同一条纪律。
|
|
352
|
+
"""
|
|
353
|
+
if not px_w or not px_h:
|
|
354
|
+
return (body_width_cm(prof) * 0.8, body_width_cm(prof) * 0.5)
|
|
355
|
+
max_w = max_w_cm or prof.get('max_image_cm') or body_width_cm(prof)
|
|
356
|
+
max_h = max_h_cm or (body_height_pt(prof) * PT_CM * 0.78) # 留出题注与上下文
|
|
357
|
+
w_cm = px_w / float(target_dpi) * 2.54
|
|
358
|
+
h_cm = px_h / float(target_dpi) * 2.54
|
|
359
|
+
scale = min(max_w / w_cm, max_h / h_cm, 1.0)
|
|
360
|
+
return (round(w_cm * scale, 3), round(h_cm * scale, 3))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def effective_dpi(px, length_cm):
|
|
364
|
+
"""图片实际打印分辨率:像素数 ÷ 显示英寸。<120 在 A4 上肉眼可见发虚。"""
|
|
365
|
+
if not px or not length_cm:
|
|
366
|
+
return 0.0
|
|
367
|
+
return px / (length_cm / 2.54)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ── 标题编号 ───────────────────────────────────────────────────────────────
|
|
371
|
+
_CN_DIGITS = '零一二三四五六七八九'
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def cn_num(n):
|
|
375
|
+
"""1→一,11→十一,21→二十一(技术文档章节到不了三位数,够用)。"""
|
|
376
|
+
n = int(n)
|
|
377
|
+
if n <= 0:
|
|
378
|
+
return str(n)
|
|
379
|
+
if n < 10:
|
|
380
|
+
return _CN_DIGITS[n]
|
|
381
|
+
if n < 20:
|
|
382
|
+
return '十' + (_CN_DIGITS[n % 10] if n % 10 else '')
|
|
383
|
+
if n < 100:
|
|
384
|
+
return _CN_DIGITS[n // 10] + '十' + (_CN_DIGITS[n % 10] if n % 10 else '')
|
|
385
|
+
return str(n)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def heading_number(prof, level, counters):
|
|
389
|
+
"""按规范给出第 level 级标题的编号文本(counters 为 1..4 级计数器列表)。"""
|
|
390
|
+
style = prof.get('numbering', 'cn')
|
|
391
|
+
if style == 'none':
|
|
392
|
+
return ''
|
|
393
|
+
if style == 'decimal':
|
|
394
|
+
return '.'.join(str(counters[i]) for i in range(level)) + ' '
|
|
395
|
+
# cn:一、/(一)/1./(1)
|
|
396
|
+
if level == 1:
|
|
397
|
+
return f'{cn_num(counters[0])}、'
|
|
398
|
+
if level == 2:
|
|
399
|
+
return f'({cn_num(counters[1])})'
|
|
400
|
+
if level == 3:
|
|
401
|
+
return f'{counters[2]}.'
|
|
402
|
+
return f'({counters[3]})'
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# 体检端要能反过来认出这些编号,判断「编号连不连续」——正则与上面的生成规则同源。
|
|
406
|
+
NUM_PATTERNS = [
|
|
407
|
+
('cn1', re.compile(r'^\s*([一二三四五六七八九十]+)\s*[、..]\s*')),
|
|
408
|
+
('cn2', re.compile(r'^\s*[((]\s*([一二三四五六七八九十]+)\s*[))]\s*')),
|
|
409
|
+
('ar1', re.compile(r'^\s*(\d+)\s*[、..]\s*(?!\d)')),
|
|
410
|
+
('ar2', re.compile(r'^\s*[((]\s*(\d+)\s*[))]\s*')),
|
|
411
|
+
('dec', re.compile(r'^\s*(\d+(?:\.\d+)+)\s*[、..]?\s+')),
|
|
412
|
+
('chap', re.compile(r'^\s*第\s*([一二三四五六七八九十\d]+)\s*[章节部分篇]\s*')),
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def parse_number(text):
|
|
417
|
+
"""从标题文本里认出编号:返回 (kind, 序号元组, 去掉编号后的正文) 或 None。"""
|
|
418
|
+
s = str(text or '')
|
|
419
|
+
for kind, pat in NUM_PATTERNS:
|
|
420
|
+
m = pat.match(s)
|
|
421
|
+
if not m:
|
|
422
|
+
continue
|
|
423
|
+
raw = m.group(1)
|
|
424
|
+
if kind == 'dec':
|
|
425
|
+
try:
|
|
426
|
+
seq = tuple(int(x) for x in raw.split('.'))
|
|
427
|
+
except ValueError:
|
|
428
|
+
continue
|
|
429
|
+
elif raw.isdigit():
|
|
430
|
+
seq = (int(raw),)
|
|
431
|
+
else:
|
|
432
|
+
seq = (cn_to_int(raw),)
|
|
433
|
+
if seq[0] <= 0:
|
|
434
|
+
continue
|
|
435
|
+
return kind, seq, s[m.end():].strip()
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def cn_to_int(s):
|
|
440
|
+
"""一→1,十一→11,二十→20。认不出返回 0。"""
|
|
441
|
+
s = str(s or '').strip()
|
|
442
|
+
if not s:
|
|
443
|
+
return 0
|
|
444
|
+
if s.isdigit():
|
|
445
|
+
return int(s)
|
|
446
|
+
if '十' not in s:
|
|
447
|
+
return _CN_DIGITS.find(s[0]) if s[0] in _CN_DIGITS else 0
|
|
448
|
+
head, _, tail = s.partition('十')
|
|
449
|
+
h = _CN_DIGITS.find(head) if head else 1
|
|
450
|
+
t = _CN_DIGITS.find(tail) if tail else 0
|
|
451
|
+
if h < 0 or t < 0:
|
|
452
|
+
return 0
|
|
453
|
+
return h * 10 + t
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def caption_label(prof, kind, index, chapter=None):
|
|
457
|
+
"""题注编号文本:图 3 / 图 2-3(按章编号)/ 表 1。"""
|
|
458
|
+
prefix = prof['fig_prefix'] if kind == 'fig' else prof['tab_prefix']
|
|
459
|
+
if prof.get('caption_by_chapter') and chapter:
|
|
460
|
+
return f'{prefix} {chapter}-{index}'
|
|
461
|
+
return f'{prefix} {index}'
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
CAPTION_RE = re.compile(r'^\s*(图|表|Figure|Fig\.?|Table)\s*([0-9]+(?:[-–.][0-9]+)*)\s*[.、::]?\s*(.*)$', re.I)
|
|
465
|
+
REF_RE = re.compile(r'(?:如|见|参见|详见)?\s*(图|表)\s*([0-9]+(?:[-–.][0-9]+)*)\s*(?:所示|所列|中)?')
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def self_test():
|
|
469
|
+
p = profile('tech')
|
|
470
|
+
assert abs(body_width_cm(p) - 14.66) < 0.01, body_width_cm(p)
|
|
471
|
+
assert wrap_count('中' * 40, '小四', body_width_pt(p)) >= 1
|
|
472
|
+
assert wrap_count('中' * 200, '小四', body_width_pt(p)) > 4
|
|
473
|
+
assert pt_of('小四') == 12 and size_name(16) == '三号'
|
|
474
|
+
assert cn_num(21) == '二十一' and cn_to_int('二十一') == 21
|
|
475
|
+
assert parse_number('一、项目背景')[1] == (1,)
|
|
476
|
+
assert parse_number('1.2.3 技术路线')[1] == (1, 2, 3)
|
|
477
|
+
assert parse_number('(三)预期成果')[1] == (3,)
|
|
478
|
+
w = fit_table_widths([['指标', '2025 年实际完成情况说明', '备注'], ['交付率', '98.2%', '—']], p)
|
|
479
|
+
assert abs(sum(w) - body_width_cm(p)) < 0.05, (w, sum(w))
|
|
480
|
+
g = profile('gov')
|
|
481
|
+
assert pt_of(g['body']['size']) == 16 and g['body']['line_rule'] == 'exact'
|
|
482
|
+
assert image_box(1600, 900, p)[0] <= body_width_cm(p) + 1e-6
|
|
483
|
+
a = image_box(4000, 1000, p)
|
|
484
|
+
assert abs(a[0] / a[1] - 4.0) < 1e-6, a # 等比,永不变形
|
|
485
|
+
print('[docx_layout] self-test OK ·',
|
|
486
|
+
f"版心 {body_width_cm(p):.2f}cm / 行高 {line_height_pt(p):.1f}pt / 缩进 {indent_pt(p):.0f}pt")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
if __name__ == '__main__':
|
|
490
|
+
self_test()
|