skilldrop-cli 0.16.3__py3-none-any.whl
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.
- skilldrop_cli/__init__.py +16 -0
- skilldrop_cli/__main__.py +3 -0
- skilldrop_cli/data/LICENSE +9 -0
- skilldrop_cli/data/LICENSE-APACHE +202 -0
- skilldrop_cli/data/LICENSE-MIT +21 -0
- skilldrop_cli/data/agents/README.md +127 -0
- skilldrop_cli/data/agents/code-quality.md +40 -0
- skilldrop_cli/data/agents/devils-advocate.md +40 -0
- skilldrop_cli/data/agents/security-reviewer.md +44 -0
- skilldrop_cli/data/bin/skilldrop.js +2110 -0
- skilldrop_cli/data/catalogue.json +187 -0
- skilldrop_cli/data/contracts/agent.schema.json +28 -0
- skilldrop_cli/data/contracts/catalogue.schema.json +33 -0
- skilldrop_cli/data/contracts/guide.schema.json +18 -0
- skilldrop_cli/data/contracts/loop.schema.json +87 -0
- skilldrop_cli/data/contracts/pack.schema.json +75 -0
- skilldrop_cli/data/contracts/skill.schema.json +110 -0
- skilldrop_cli/data/contracts/terminals.json +33 -0
- skilldrop_cli/data/model-routing.json +474 -0
- skilldrop_cli/data/package.json +46 -0
- skilldrop_cli/data/packs/ai-engineering/pack.json +57 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-adoption-stage/SKILL.md +90 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-adoption-stage/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-adoption-stage/evals/evals.json +19 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-adoption-stage/manifest.json +39 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-budget/SKILL.md +63 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-budget/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-budget/evals/evals.json +28 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-budget/manifest.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-budget/templates/budget-spec.md +30 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-loop-design/SKILL.md +70 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-loop-design/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-loop-design/evals/evals.json +28 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-loop-design/manifest.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-loop-design/templates/loop-spec.md +46 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/SKILL.md +89 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/evals/eval_queries.json +15 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/evals/evals.json +41 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/examples/support-triage-agent.md +117 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/manifest.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/reference.md +101 -0
- skilldrop_cli/data/packs/ai-engineering/skills/agent-threat-model/templates/agent-threat-model.md +71 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-adoption-rollout/SKILL.md +61 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-adoption-rollout/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-adoption-rollout/evals/evals.json +19 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-adoption-rollout/manifest.json +33 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-readiness-assessment/SKILL.md +68 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-readiness-assessment/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-readiness-assessment/evals/evals.json +17 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-readiness-assessment/manifest.json +35 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-policy/SKILL.md +67 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-policy/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-policy/evals/evals.json +19 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-policy/manifest.json +33 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/SKILL.md +111 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/evals/evals.json +19 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/evals/files/1/northwind-ai-events.csv +4213 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +133 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/manifest.json +12 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/scripts/build_report.py +436 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/templates/report-per-user.md +42 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/templates/report-team-rollup.md +49 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +76 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-use-case-triage/SKILL.md +59 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-use-case-triage/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-use-case-triage/evals/evals.json +18 -0
- skilldrop_cli/data/packs/ai-engineering/skills/ai-use-case-triage/manifest.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/SKILL.md +73 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/evals/evals.json +19 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/manifest.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/reference.md +58 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/templates/eval-plan.md +63 -0
- skilldrop_cli/data/packs/ai-engineering/skills/llm-eval-harness/templates/golden-case.jsonl +5 -0
- skilldrop_cli/data/packs/ai-engineering/skills/subagent-design/SKILL.md +69 -0
- skilldrop_cli/data/packs/ai-engineering/skills/subagent-design/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/subagent-design/evals/evals.json +29 -0
- skilldrop_cli/data/packs/ai-engineering/skills/subagent-design/manifest.json +11 -0
- skilldrop_cli/data/packs/ai-engineering/skills/subagent-design/templates/orchestration-plan.md +47 -0
- skilldrop_cli/data/packs/api-builder/pack.json +48 -0
- skilldrop_cli/data/packs/api-builder/skills/eval-harness-generator/SKILL.md +84 -0
- skilldrop_cli/data/packs/api-builder/skills/eval-harness-generator/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/api-builder/skills/eval-harness-generator/evals/evals.json +19 -0
- skilldrop_cli/data/packs/api-builder/skills/eval-harness-generator/evals/files/1/SKILL.md +31 -0
- skilldrop_cli/data/packs/api-builder/skills/eval-harness-generator/manifest.json +11 -0
- skilldrop_cli/data/packs/api-builder/skills/prompt-caching-advisor/SKILL.md +90 -0
- skilldrop_cli/data/packs/api-builder/skills/prompt-caching-advisor/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/api-builder/skills/prompt-caching-advisor/evals/evals.json +19 -0
- skilldrop_cli/data/packs/api-builder/skills/prompt-caching-advisor/manifest.json +11 -0
- skilldrop_cli/data/packs/api-builder/skills/token-budget-estimator/SKILL.md +75 -0
- skilldrop_cli/data/packs/api-builder/skills/token-budget-estimator/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/api-builder/skills/token-budget-estimator/evals/evals.json +18 -0
- skilldrop_cli/data/packs/api-builder/skills/token-budget-estimator/manifest.json +11 -0
- skilldrop_cli/data/packs/api-builder/skills/tool-use-schema-writer/SKILL.md +111 -0
- skilldrop_cli/data/packs/api-builder/skills/tool-use-schema-writer/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/api-builder/skills/tool-use-schema-writer/evals/evals.json +18 -0
- skilldrop_cli/data/packs/api-builder/skills/tool-use-schema-writer/manifest.json +11 -0
- skilldrop_cli/data/packs/converters/pack.json +66 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/SKILL.md +103 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/evals/evals.json +27 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/evals/files/1/northwind-q3.pptx +0 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/evals/files/2/acme-contract-summary.pdf +93 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/examples/inputs/accounts.json +1 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/examples/inputs/fabrikam-status.html +14 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/examples/mixed-inputs.md +306 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/manifest.json +49 -0
- skilldrop_cli/data/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +1299 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/SKILL.md +124 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/eval_queries.json +10 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/evals.json +29 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/files/1/brand/brand.json +80 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/files/1/brand/logo-white.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/files/1/brand/logo.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/files/1/brand/mark.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/evals/files/1/docs/vendor-onboarding.md +42 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/examples/q3-support-review-input.md +54 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/examples/q3-support-review.md +80 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/examples/tickets-by-region.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/manifest.json +52 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/reference.md +89 -0
- skilldrop_cli/data/packs/converters/skills/md-to-docx/scripts/md_to_docx.py +1114 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/SKILL.md +100 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/evals.json +18 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/files/1/brand/brand.json +80 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/files/1/brand/logo-white.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/files/1/brand/logo.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/files/1/brand/mark.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/files/1/docs/q3-review.md +35 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/evals/files/1/img/dashboard.png +0 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/examples/inputs/chart.svg +8 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/examples/inputs/ops-review.md +48 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/examples/ops-review.md +83 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/manifest.json +43 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/reference.md +70 -0
- skilldrop_cli/data/packs/converters/skills/md-to-html/scripts/md_to_html.py +652 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/SKILL.md +120 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/evals/eval_queries.json +9 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/evals/evals.json +28 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/evals/files/1/exports/northwind-invoices.csv +25 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/evals/files/2/reports/q3-ops-review.md +32 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/examples/vendor-review-input.md +28 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/examples/vendor-review.md +94 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/manifest.json +33 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/reference.md +94 -0
- skilldrop_cli/data/packs/converters/skills/md-to-xlsx/scripts/md_to_xlsx.py +667 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/SKILL.md +109 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/evals/evals.json +17 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/evals/files/1/docs/contoso-orders.md +23 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/broken-diagrams.md +147 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/inputs/broken-flowchart.mmd +8 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/inputs/broken-sequence.mmd +11 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/inputs/checkout-flow.mmd +9 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/inputs/design-notes.md +35 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/inputs/fixed-flowchart.mmd +9 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/examples/inputs/typo.mmd +2 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/manifest.json +40 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/reference.md +70 -0
- skilldrop_cli/data/packs/converters/skills/mermaid-render/scripts/render_mermaid.py +486 -0
- skilldrop_cli/data/packs/core/loops/ship-a-draft/LOOP.md +77 -0
- skilldrop_cli/data/packs/core/loops/ship-a-draft/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/core/loops/ship-a-draft/evals/evals.json +25 -0
- skilldrop_cli/data/packs/core/loops/ship-a-draft/loop.json +41 -0
- skilldrop_cli/data/packs/core/pack.json +50 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/SKILL.md +79 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/evals/evals.json +16 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/manifest.json +11 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-adr.md +41 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-decision-log.md +35 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-deck.md +43 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-design-doc.md +48 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-exec-summary.md +47 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-generic.md +45 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-runbook.md +44 -0
- skilldrop_cli/data/packs/core/skills/brief-intake/templates/brief-tech-comparison.md +38 -0
- skilldrop_cli/data/packs/core/skills/council-review/SKILL.md +106 -0
- skilldrop_cli/data/packs/core/skills/council-review/evals/eval_queries.json +38 -0
- skilldrop_cli/data/packs/core/skills/council-review/evals/evals.json +18 -0
- skilldrop_cli/data/packs/core/skills/council-review/examples/redis-cache-decision.md +75 -0
- skilldrop_cli/data/packs/core/skills/council-review/manifest.json +11 -0
- skilldrop_cli/data/packs/core/skills/council-review/reference.md +76 -0
- skilldrop_cli/data/packs/core/skills/council-review/seats/architect.md +29 -0
- skilldrop_cli/data/packs/core/skills/council-review/seats/bench.md +39 -0
- skilldrop_cli/data/packs/core/skills/council-review/seats/operator.md +27 -0
- skilldrop_cli/data/packs/core/skills/council-review/seats/pragmatist.md +29 -0
- skilldrop_cli/data/packs/core/skills/council-review/seats/security-engineer.md +29 -0
- skilldrop_cli/data/packs/core/skills/council-review/seats/user-advocate.md +27 -0
- skilldrop_cli/data/packs/core/skills/council-review/templates/council-review.md +59 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/SKILL.md +119 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/evals/eval_queries.json +38 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/evals/evals.json +17 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/examples/design-doc-critique.md +34 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/manifest.json +11 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/adr.md +36 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/decision-log.md +39 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/deck.md +45 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/design-doc.md +42 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/exec-summary.md +41 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/generic.md +40 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/runbook.md +42 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/rubrics/tech-comparison.md +40 -0
- skilldrop_cli/data/packs/core/skills/doc-critique/templates/critique.md +49 -0
- skilldrop_cli/data/packs/core/skills/output-hygiene/SKILL.md +97 -0
- skilldrop_cli/data/packs/core/skills/output-hygiene/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/core/skills/output-hygiene/evals/evals.json +30 -0
- skilldrop_cli/data/packs/core/skills/output-hygiene/manifest.json +34 -0
- skilldrop_cli/data/packs/core/skills/output-hygiene/reference.md +106 -0
- skilldrop_cli/data/packs/core/skills/output-hygiene/scripts/scrub.py +315 -0
- skilldrop_cli/data/packs/data-analytics/pack.json +57 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/SKILL.md +83 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/evals/evals.json +19 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/examples/support-staffing.md +84 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/manifest.json +40 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/reference.md +45 -0
- skilldrop_cli/data/packs/data-analytics/skills/dashboard-spec/templates/dashboard-spec.md +55 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/SKILL.md +84 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/evals/evals.json +19 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/examples/refund-rate.md +104 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/manifest.json +41 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/reference.md +67 -0
- skilldrop_cli/data/packs/data-analytics/skills/metric-definition/templates/metric-spec.md +70 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/SKILL.md +83 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/evals/evals.json +19 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/examples/revenue-by-region.md +174 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/manifest.json +42 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/reference.md +84 -0
- skilldrop_cli/data/packs/data-analytics/skills/sql-review/scripts/sql_lint.py +461 -0
- skilldrop_cli/data/packs/design/pack.json +60 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/SKILL.md +105 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/evals/evals.json +18 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/evals/files/1/brand/northwind-logo-white.png +0 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/evals/files/1/brand/northwind-logo.png +0 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/examples/northwind-brand.md +48 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/manifest.json +45 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/scripts/check_brand.py +114 -0
- skilldrop_cli/data/packs/design/skills/brand-kit/templates/brand.json +54 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/SKILL.md +201 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/evals/eval_queries.json +14 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/evals/evals.json +40 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/examples/exec-board-update.md +108 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/examples/templated-brand-deck.md +148 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/manifest.json +18 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/reference.md +180 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/requirements.txt +3 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/scripts/build_deck.py +1134 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/templates/deck-spec.json +168 -0
- skilldrop_cli/data/packs/design/skills/deck-builder/templates/palettes.json +59 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/SKILL.md +113 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/evals/evals.json +18 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/evals/files/1/brand/brand.json +80 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/evals/files/1/brand/logo-white.png +0 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/evals/files/1/brand/logo.png +0 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/evals/files/1/brand/mark.png +0 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/examples/community-health-fair.md +68 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/manifest.json +40 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/reference.md +54 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/scripts/build_flyer.py +323 -0
- skilldrop_cli/data/packs/design/skills/marketing-flyer/templates/flyer-spec.json +26 -0
- skilldrop_cli/data/packs/design/skills/slide-outliner/SKILL.md +58 -0
- skilldrop_cli/data/packs/design/skills/slide-outliner/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/design/skills/slide-outliner/evals/evals.json +16 -0
- skilldrop_cli/data/packs/design/skills/slide-outliner/manifest.json +11 -0
- skilldrop_cli/data/packs/design/skills/slide-outliner/templates/deck-outline.md +131 -0
- skilldrop_cli/data/packs/dev-team/loops/build/LOOP.md +80 -0
- skilldrop_cli/data/packs/dev-team/loops/build/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/dev-team/loops/build/evals/evals.json +27 -0
- skilldrop_cli/data/packs/dev-team/loops/build/loop.json +48 -0
- skilldrop_cli/data/packs/dev-team/loops/release/LOOP.md +91 -0
- skilldrop_cli/data/packs/dev-team/loops/release/loop.json +47 -0
- skilldrop_cli/data/packs/dev-team/pack.json +56 -0
- skilldrop_cli/data/packs/dev-team/skills/accessibility-audit/SKILL.md +74 -0
- skilldrop_cli/data/packs/dev-team/skills/accessibility-audit/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/dev-team/skills/accessibility-audit/evals/evals.json +19 -0
- skilldrop_cli/data/packs/dev-team/skills/accessibility-audit/examples/login-form.md +76 -0
- skilldrop_cli/data/packs/dev-team/skills/accessibility-audit/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/accessibility-audit/reference.md +69 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/SKILL.md +82 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/eval_queries.json +15 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/evals.json +50 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/1/Makefile +5 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/1/alembic/versions/0001_create_bookings.py +10 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/1/booking/__init__.py +0 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/1/booking/app.py +8 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/1/pyproject.toml +11 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/1/tests/test_health.py +5 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/2/AGENTS.md +3 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/2/package.json +12 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/evals/files/2/src/index.ts +1 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/reference.md +89 -0
- skilldrop_cli/data/packs/dev-team/skills/agents-md-generator/templates/agents-md.md +48 -0
- skilldrop_cli/data/packs/dev-team/skills/bug-triage/SKILL.md +67 -0
- skilldrop_cli/data/packs/dev-team/skills/bug-triage/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/dev-team/skills/bug-triage/evals/evals.json +17 -0
- skilldrop_cli/data/packs/dev-team/skills/bug-triage/manifest.json +14 -0
- skilldrop_cli/data/packs/dev-team/skills/bug-triage/templates/bug-ticket.md +86 -0
- skilldrop_cli/data/packs/dev-team/skills/contribution-wizard/SKILL.md +134 -0
- skilldrop_cli/data/packs/dev-team/skills/contribution-wizard/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/dev-team/skills/contribution-wizard/evals/evals.json +19 -0
- skilldrop_cli/data/packs/dev-team/skills/contribution-wizard/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/SKILL.md +143 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/evals/eval_queries.json +38 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/evals/evals.json +19 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/examples/rate-limiter-review.md +46 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/lenses/adversarial-review.md +88 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/lenses/edge-cases.md +95 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/lenses/future-proofing.md +75 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/lenses/test-coverage.md +90 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/manifest.json +14 -0
- skilldrop_cli/data/packs/dev-team/skills/devils-advocate/templates/challenge.md +118 -0
- skilldrop_cli/data/packs/dev-team/skills/feature-implement-loop/SKILL.md +88 -0
- skilldrop_cli/data/packs/dev-team/skills/feature-implement-loop/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/dev-team/skills/feature-implement-loop/evals/evals.json +19 -0
- skilldrop_cli/data/packs/dev-team/skills/feature-implement-loop/manifest.json +14 -0
- skilldrop_cli/data/packs/dev-team/skills/launch-readiness/SKILL.md +95 -0
- skilldrop_cli/data/packs/dev-team/skills/launch-readiness/evals/eval_queries.json +10 -0
- skilldrop_cli/data/packs/dev-team/skills/launch-readiness/evals/evals.json +19 -0
- skilldrop_cli/data/packs/dev-team/skills/launch-readiness/examples/schema-change-revise.md +54 -0
- skilldrop_cli/data/packs/dev-team/skills/launch-readiness/manifest.json +24 -0
- skilldrop_cli/data/packs/dev-team/skills/launch-readiness/templates/readiness-report.md +41 -0
- skilldrop_cli/data/packs/dev-team/skills/migration-plan/SKILL.md +68 -0
- skilldrop_cli/data/packs/dev-team/skills/migration-plan/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/dev-team/skills/migration-plan/evals/evals.json +16 -0
- skilldrop_cli/data/packs/dev-team/skills/migration-plan/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/migration-plan/templates/migration-plan.md +68 -0
- skilldrop_cli/data/packs/dev-team/skills/pre-merge-review/SKILL.md +85 -0
- skilldrop_cli/data/packs/dev-team/skills/pre-merge-review/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/pre-merge-review/evals/evals.json +26 -0
- skilldrop_cli/data/packs/dev-team/skills/pre-merge-review/examples/pre-merge-verdict.md +43 -0
- skilldrop_cli/data/packs/dev-team/skills/pre-merge-review/manifest.json +15 -0
- skilldrop_cli/data/packs/dev-team/skills/pre-merge-review/scripts/gate.py +189 -0
- skilldrop_cli/data/packs/dev-team/skills/release-notes/SKILL.md +61 -0
- skilldrop_cli/data/packs/dev-team/skills/release-notes/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/dev-team/skills/release-notes/evals/evals.json +19 -0
- skilldrop_cli/data/packs/dev-team/skills/release-notes/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/release-notes/templates/release-notes.md +83 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/SKILL.md +170 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/evals/evals.json +16 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/reference.md +151 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/templates/github-actions-sonar-server.yml.tmpl +50 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/templates/github-actions-sonarcloud.yml.tmpl +53 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/templates/readme-snippet.md +38 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-onboard/templates/sonar-project.properties.tmpl +31 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/SKILL.md +220 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/evals/evals.json +16 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/lenses/bugs.md +48 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/lenses/code-smells.md +64 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/lenses/coverage-and-duplication.md +74 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/lenses/security-hotspots.md +56 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/lenses/vulnerabilities.md +52 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/manifest.json +37 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/reference.md +167 -0
- skilldrop_cli/data/packs/dev-team/skills/sonar-review/templates/report.md +84 -0
- skilldrop_cli/data/packs/dev-team/skills/test-plan-generator/SKILL.md +59 -0
- skilldrop_cli/data/packs/dev-team/skills/test-plan-generator/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/dev-team/skills/test-plan-generator/evals/evals.json +16 -0
- skilldrop_cli/data/packs/dev-team/skills/test-plan-generator/manifest.json +11 -0
- skilldrop_cli/data/packs/dev-team/skills/test-plan-generator/templates/test-plan.md +83 -0
- skilldrop_cli/data/packs/dev-team/skills/user-story-splitter/SKILL.md +88 -0
- skilldrop_cli/data/packs/dev-team/skills/user-story-splitter/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/dev-team/skills/user-story-splitter/evals/evals.json +16 -0
- skilldrop_cli/data/packs/dev-team/skills/user-story-splitter/examples/notifications-epic.md +123 -0
- skilldrop_cli/data/packs/dev-team/skills/user-story-splitter/manifest.json +14 -0
- skilldrop_cli/data/packs/dev-team/skills/user-story-splitter/templates/story.md +40 -0
- skilldrop_cli/data/packs/experience-design/pack.json +68 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/SKILL.md +77 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/evals/evals.json +18 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/examples/parking-permit-page.md +102 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/manifest.json +41 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/scripts/readability.py +115 -0
- skilldrop_cli/data/packs/experience-design/skills/content-design/templates/content-spec.md +45 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/SKILL.md +79 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/evals/evals.json +19 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/examples/text-field-spec.md +121 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/manifest.json +46 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/reference.md +66 -0
- skilldrop_cli/data/packs/experience-design/skills/design-system-spec/templates/component-spec.md +74 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/SKILL.md +76 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/evals/evals.json +19 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/examples/payroll-app-nav.md +127 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/manifest.json +46 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/reference.md +54 -0
- skilldrop_cli/data/packs/experience-design/skills/information-architecture/templates/ia-spec.md +77 -0
- skilldrop_cli/data/packs/experience-design/skills/service-blueprint/SKILL.md +76 -0
- skilldrop_cli/data/packs/experience-design/skills/service-blueprint/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/experience-design/skills/service-blueprint/evals/evals.json +19 -0
- skilldrop_cli/data/packs/experience-design/skills/service-blueprint/examples/insurance-claim-blueprint.md +107 -0
- skilldrop_cli/data/packs/experience-design/skills/service-blueprint/manifest.json +45 -0
- skilldrop_cli/data/packs/experience-design/skills/service-blueprint/templates/blueprint.md +72 -0
- skilldrop_cli/data/packs/experience-design/skills/ux-writing/SKILL.md +77 -0
- skilldrop_cli/data/packs/experience-design/skills/ux-writing/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/experience-design/skills/ux-writing/evals/evals.json +19 -0
- skilldrop_cli/data/packs/experience-design/skills/ux-writing/examples/team-invite-flow.md +66 -0
- skilldrop_cli/data/packs/experience-design/skills/ux-writing/manifest.json +41 -0
- skilldrop_cli/data/packs/experience-design/skills/ux-writing/templates/string-table.md +22 -0
- skilldrop_cli/data/packs/grc/pack.json +57 -0
- skilldrop_cli/data/packs/grc/skills/dpia/SKILL.md +123 -0
- skilldrop_cli/data/packs/grc/skills/dpia/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/grc/skills/dpia/evals/evals.json +19 -0
- skilldrop_cli/data/packs/grc/skills/dpia/examples/acme-warehouse-performance.md +159 -0
- skilldrop_cli/data/packs/grc/skills/dpia/manifest.json +47 -0
- skilldrop_cli/data/packs/grc/skills/dpia/reference.md +118 -0
- skilldrop_cli/data/packs/grc/skills/dpia/templates/dpia.md +103 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/SKILL.md +139 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/evals/evals.json +19 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/examples/northwind-register.csv +9 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/examples/northwind-register.md +127 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/manifest.json +43 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/reference.md +99 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/scripts/check_register.py +338 -0
- skilldrop_cli/data/packs/grc/skills/risk-register/templates/risk-register.csv +2 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/SKILL.md +117 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/evals/evals.json +19 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/examples/northwind-evidence-map.csv +17 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/examples/northwind-type2-security.md +127 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/manifest.json +41 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/reference.md +156 -0
- skilldrop_cli/data/packs/grc/skills/soc2-evidence-map/templates/evidence-map.csv +2 -0
- skilldrop_cli/data/packs/infra-as-code/pack.json +51 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/SKILL.md +85 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/evals/evals.json +31 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket/README.md +71 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket/examples/basic/main.tf +30 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket/main.tf +153 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket/outputs.tf +14 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket/variables.tf +71 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket/versions.tf +10 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/examples/s3-log-bucket.md +50 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/manifest.json +41 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/reference.md +105 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-module/templates/module-readme.md +55 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/SKILL.md +87 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/evals/evals.json +30 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/evals/files/2/plan.json +133 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/examples/northwind-orders-plan.json +174 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/examples/northwind-orders.md +87 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/manifest.json +48 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/reference.md +77 -0
- skilldrop_cli/data/packs/infra-as-code/skills/terraform-plan-review/scripts/plan_summary.py +361 -0
- skilldrop_cli/data/packs/product-manager/loops/discover/LOOP.md +81 -0
- skilldrop_cli/data/packs/product-manager/loops/discover/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/product-manager/loops/discover/evals/evals.json +26 -0
- skilldrop_cli/data/packs/product-manager/loops/discover/loop.json +41 -0
- skilldrop_cli/data/packs/product-manager/pack.json +61 -0
- skilldrop_cli/data/packs/product-manager/skills/business-case/SKILL.md +64 -0
- skilldrop_cli/data/packs/product-manager/skills/business-case/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/product-manager/skills/business-case/evals/evals.json +19 -0
- skilldrop_cli/data/packs/product-manager/skills/business-case/examples/build-vs-buy-flags.md +42 -0
- skilldrop_cli/data/packs/product-manager/skills/business-case/manifest.json +11 -0
- skilldrop_cli/data/packs/product-manager/skills/business-case/templates/business-case.md +60 -0
- skilldrop_cli/data/packs/product-manager/skills/okr-cascade/SKILL.md +59 -0
- skilldrop_cli/data/packs/product-manager/skills/okr-cascade/evals/eval_queries.json +12 -0
- skilldrop_cli/data/packs/product-manager/skills/okr-cascade/evals/evals.json +28 -0
- skilldrop_cli/data/packs/product-manager/skills/okr-cascade/manifest.json +11 -0
- skilldrop_cli/data/packs/product-manager/skills/okr-cascade/templates/okr-cascade.md +46 -0
- skilldrop_cli/data/packs/product-manager/skills/prd-draft/SKILL.md +62 -0
- skilldrop_cli/data/packs/product-manager/skills/prd-draft/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/product-manager/skills/prd-draft/evals/evals.json +17 -0
- skilldrop_cli/data/packs/product-manager/skills/prd-draft/manifest.json +16 -0
- skilldrop_cli/data/packs/product-manager/skills/prd-draft/templates/prd.md +66 -0
- skilldrop_cli/data/packs/product-manager/skills/prfaq/SKILL.md +76 -0
- skilldrop_cli/data/packs/product-manager/skills/prfaq/evals/eval_queries.json +13 -0
- skilldrop_cli/data/packs/product-manager/skills/prfaq/evals/evals.json +32 -0
- skilldrop_cli/data/packs/product-manager/skills/prfaq/manifest.json +11 -0
- skilldrop_cli/data/packs/product-manager/skills/prfaq/templates/prfaq.md +60 -0
- skilldrop_cli/data/packs/product-manager/skills/requirements-interview/SKILL.md +61 -0
- skilldrop_cli/data/packs/product-manager/skills/requirements-interview/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/product-manager/skills/requirements-interview/evals/evals.json +16 -0
- skilldrop_cli/data/packs/product-manager/skills/requirements-interview/manifest.json +14 -0
- skilldrop_cli/data/packs/product-manager/skills/requirements-interview/templates/interview-kit.md +63 -0
- skilldrop_cli/data/packs/product-manager/skills/strategy-analysis/SKILL.md +63 -0
- skilldrop_cli/data/packs/product-manager/skills/strategy-analysis/evals/eval_queries.json +13 -0
- skilldrop_cli/data/packs/product-manager/skills/strategy-analysis/evals/evals.json +36 -0
- skilldrop_cli/data/packs/product-manager/skills/strategy-analysis/examples/market-entry-tows.md +42 -0
- skilldrop_cli/data/packs/product-manager/skills/strategy-analysis/manifest.json +15 -0
- skilldrop_cli/data/packs/product-manager/skills/success-metrics/SKILL.md +65 -0
- skilldrop_cli/data/packs/product-manager/skills/success-metrics/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/product-manager/skills/success-metrics/evals/evals.json +27 -0
- skilldrop_cli/data/packs/product-manager/skills/success-metrics/manifest.json +11 -0
- skilldrop_cli/data/packs/product-manager/skills/success-metrics/templates/metrics-plan.md +55 -0
- skilldrop_cli/data/packs/product-manager/skills/user-journey-map/SKILL.md +65 -0
- skilldrop_cli/data/packs/product-manager/skills/user-journey-map/evals/eval_queries.json +12 -0
- skilldrop_cli/data/packs/product-manager/skills/user-journey-map/evals/evals.json +30 -0
- skilldrop_cli/data/packs/product-manager/skills/user-journey-map/examples/b2b-saas-onboarding.md +69 -0
- skilldrop_cli/data/packs/product-manager/skills/user-journey-map/manifest.json +15 -0
- skilldrop_cli/data/packs/product-manager/skills/user-journey-map/templates/journey-map.md +47 -0
- skilldrop_cli/data/packs/research/pack.json +52 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/SKILL.md +131 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/evals/evals.json +19 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/examples/conversion-drop.md +132 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/manifest.json +43 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/reference.md +107 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +204 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/templates/hypothesis-comparison.md +61 -0
- skilldrop_cli/data/packs/research/skills/hypothesis-comparison/templates/matrix.csv +4 -0
- skilldrop_cli/data/packs/research/skills/research-plan/SKILL.md +122 -0
- skilldrop_cli/data/packs/research/skills/research-plan/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/research/skills/research-plan/evals/evals.json +28 -0
- skilldrop_cli/data/packs/research/skills/research-plan/examples/four-day-week-plan.md +97 -0
- skilldrop_cli/data/packs/research/skills/research-plan/manifest.json +47 -0
- skilldrop_cli/data/packs/research/skills/research-plan/reference.md +88 -0
- skilldrop_cli/data/packs/research/skills/research-plan/templates/research-plan.md +54 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/SKILL.md +123 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/evals/evals.json +19 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/examples/four-day-week.md +146 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/manifest.json +42 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/reference.md +92 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/scripts/citations.py +314 -0
- skilldrop_cli/data/packs/research/skills/source-synthesis/templates/synthesis.md +46 -0
- skilldrop_cli/data/packs/skill-engineering/pack.json +46 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/SKILL.md +139 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/evals/eval_queries.json +10 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/evals/evals.json +19 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/examples/release-notes-skill.md +185 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/manifest.json +42 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/reference.md +144 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/templates/eval_queries.json +9 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/templates/evals.json +16 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-author/templates/skill-template.md +52 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/SKILL.md +131 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/evals/eval_queries.json +11 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/evals/evals.json +19 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/evals/files/1/skills/changelog-writer/SKILL.md +10 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/evals/files/1/skills/pr-summary/SKILL.md +12 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/evals/files/1/skills/pr-summary/references/tone.md +3 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/evals/files/1/skills/pr-summary/scripts/collect.py +6 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/examples/meeting-notes-review.md +147 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/manifest.json +50 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/reference.md +135 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/scripts/lint_skill.py +481 -0
- skilldrop_cli/data/packs/skill-engineering/skills/skill-review/templates/review.md +43 -0
- skilldrop_cli/data/packs/solution-architect/loops/design/LOOP.md +83 -0
- skilldrop_cli/data/packs/solution-architect/loops/design/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/solution-architect/loops/design/evals/evals.json +26 -0
- skilldrop_cli/data/packs/solution-architect/loops/design/loop.json +47 -0
- skilldrop_cli/data/packs/solution-architect/pack.json +65 -0
- skilldrop_cli/data/packs/solution-architect/skills/adr-generator/SKILL.md +50 -0
- skilldrop_cli/data/packs/solution-architect/skills/adr-generator/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/adr-generator/evals/evals.json +17 -0
- skilldrop_cli/data/packs/solution-architect/skills/adr-generator/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/adr-generator/templates/madr.md +56 -0
- skilldrop_cli/data/packs/solution-architect/skills/adr-generator/templates/nygard.md +19 -0
- skilldrop_cli/data/packs/solution-architect/skills/api-contract-draft/SKILL.md +69 -0
- skilldrop_cli/data/packs/solution-architect/skills/api-contract-draft/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/api-contract-draft/evals/evals.json +18 -0
- skilldrop_cli/data/packs/solution-architect/skills/api-contract-draft/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/api-contract-draft/reference.md +73 -0
- skilldrop_cli/data/packs/solution-architect/skills/api-contract-draft/templates/openapi-skeleton.yaml +148 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/SKILL.md +66 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/evals/evals.json +16 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/examples/aws-three-tier.md +44 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/examples/c4-container-saas.md +51 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/examples/microservices-sequence.md +49 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/manifest.json +31 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/templates/c4-container.puml +31 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/templates/mermaid-aws.md +52 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/templates/mermaid-flowchart.md +37 -0
- skilldrop_cli/data/packs/solution-architect/skills/architecture-diagrams/templates/mermaid-sequence.md +39 -0
- skilldrop_cli/data/packs/solution-architect/skills/data-contract/SKILL.md +67 -0
- skilldrop_cli/data/packs/solution-architect/skills/data-contract/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/data-contract/evals/evals.json +16 -0
- skilldrop_cli/data/packs/solution-architect/skills/data-contract/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/data-contract/reference.md +46 -0
- skilldrop_cli/data/packs/solution-architect/skills/data-contract/templates/data-contract.md +69 -0
- skilldrop_cli/data/packs/solution-architect/skills/db-schema-design/SKILL.md +68 -0
- skilldrop_cli/data/packs/solution-architect/skills/db-schema-design/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/db-schema-design/evals/evals.json +16 -0
- skilldrop_cli/data/packs/solution-architect/skills/db-schema-design/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/db-schema-design/reference.md +56 -0
- skilldrop_cli/data/packs/solution-architect/skills/db-schema-design/templates/schema-design.md +75 -0
- skilldrop_cli/data/packs/solution-architect/skills/design-doc/SKILL.md +49 -0
- skilldrop_cli/data/packs/solution-architect/skills/design-doc/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/design-doc/evals/evals.json +17 -0
- skilldrop_cli/data/packs/solution-architect/skills/design-doc/manifest.json +14 -0
- skilldrop_cli/data/packs/solution-architect/skills/design-doc/templates/design-doc.md +98 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/SKILL.md +87 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/evals/evals.json +15 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/manifest.json +36 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/reference.md +91 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/requirements.txt +3 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/scripts/_figma_client.py +71 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/scripts/frame_to_mermaid.py +102 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/scripts/inspect_file.py +46 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/scripts/list_comments.py +35 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/scripts/post_comment.py +52 -0
- skilldrop_cli/data/packs/solution-architect/skills/figma-diagrams/templates/figjam-import.json +20 -0
- skilldrop_cli/data/packs/solution-architect/skills/nfr-spec/SKILL.md +63 -0
- skilldrop_cli/data/packs/solution-architect/skills/nfr-spec/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/nfr-spec/evals/evals.json +16 -0
- skilldrop_cli/data/packs/solution-architect/skills/nfr-spec/manifest.json +16 -0
- skilldrop_cli/data/packs/solution-architect/skills/nfr-spec/reference.md +74 -0
- skilldrop_cli/data/packs/solution-architect/skills/nfr-spec/templates/nfr-spec.md +56 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/SKILL.md +103 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/evals/evals.json +16 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/reference.md +150 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/templates/description.md +52 -0
- skilldrop_cli/data/packs/solution-architect/skills/reverse-architecture/templates/extraction.md +55 -0
- skilldrop_cli/data/packs/solution-architect/skills/tech-comparison-matrix/SKILL.md +56 -0
- skilldrop_cli/data/packs/solution-architect/skills/tech-comparison-matrix/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/solution-architect/skills/tech-comparison-matrix/evals/evals.json +18 -0
- skilldrop_cli/data/packs/solution-architect/skills/tech-comparison-matrix/examples/queue-selection.md +41 -0
- skilldrop_cli/data/packs/solution-architect/skills/tech-comparison-matrix/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/tech-comparison-matrix/templates/matrix.md +45 -0
- skilldrop_cli/data/packs/solution-architect/skills/threat-model/SKILL.md +65 -0
- skilldrop_cli/data/packs/solution-architect/skills/threat-model/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/solution-architect/skills/threat-model/evals/evals.json +18 -0
- skilldrop_cli/data/packs/solution-architect/skills/threat-model/examples/document-upload.md +56 -0
- skilldrop_cli/data/packs/solution-architect/skills/threat-model/manifest.json +11 -0
- skilldrop_cli/data/packs/solution-architect/skills/threat-model/reference.md +69 -0
- skilldrop_cli/data/packs/sre-oncall/loops/operate/LOOP.md +84 -0
- skilldrop_cli/data/packs/sre-oncall/loops/operate/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/sre-oncall/loops/operate/evals/evals.json +25 -0
- skilldrop_cli/data/packs/sre-oncall/loops/operate/loop.json +41 -0
- skilldrop_cli/data/packs/sre-oncall/pack.json +59 -0
- skilldrop_cli/data/packs/sre-oncall/skills/capacity-cost-model/SKILL.md +65 -0
- skilldrop_cli/data/packs/sre-oncall/skills/capacity-cost-model/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/capacity-cost-model/evals/evals.json +19 -0
- skilldrop_cli/data/packs/sre-oncall/skills/capacity-cost-model/manifest.json +11 -0
- skilldrop_cli/data/packs/sre-oncall/skills/capacity-cost-model/reference.md +54 -0
- skilldrop_cli/data/packs/sre-oncall/skills/capacity-cost-model/templates/cost-model.md +68 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/SKILL.md +121 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/evals/evals.json +29 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/examples/acme-aws-september.md +247 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/examples/acme-cur-2026-08-09.csv +1085 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/manifest.json +49 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/reference.md +100 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +630 -0
- skilldrop_cli/data/packs/sre-oncall/skills/cloud-cost-review/templates/cost-review.md +36 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/SKILL.md +114 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/evals/evals.json +28 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/evals/files/1/deploys.csv +49 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/evals/files/1/incidents.csv +7 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/evals/files/1/prs.csv +84 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/examples/checkout-api-8-weeks.md +123 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/examples/deployments.csv +49 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/examples/incidents.csv +7 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/examples/prs.csv +84 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +48 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/reference.md +83 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/scripts/delivery_metrics.py +566 -0
- skilldrop_cli/data/packs/sre-oncall/skills/delivery-metrics-report/templates/delivery-report.md +31 -0
- skilldrop_cli/data/packs/sre-oncall/skills/incident-comms/SKILL.md +68 -0
- skilldrop_cli/data/packs/sre-oncall/skills/incident-comms/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/incident-comms/evals/evals.json +16 -0
- skilldrop_cli/data/packs/sre-oncall/skills/incident-comms/manifest.json +14 -0
- skilldrop_cli/data/packs/sre-oncall/skills/incident-comms/reference.md +43 -0
- skilldrop_cli/data/packs/sre-oncall/skills/incident-comms/templates/messages.md +82 -0
- skilldrop_cli/data/packs/sre-oncall/skills/observability-plan/SKILL.md +70 -0
- skilldrop_cli/data/packs/sre-oncall/skills/observability-plan/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/observability-plan/evals/evals.json +16 -0
- skilldrop_cli/data/packs/sre-oncall/skills/observability-plan/manifest.json +15 -0
- skilldrop_cli/data/packs/sre-oncall/skills/observability-plan/reference.md +67 -0
- skilldrop_cli/data/packs/sre-oncall/skills/observability-plan/templates/observability-plan.md +62 -0
- skilldrop_cli/data/packs/sre-oncall/skills/postmortem-generator/SKILL.md +66 -0
- skilldrop_cli/data/packs/sre-oncall/skills/postmortem-generator/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/postmortem-generator/evals/evals.json +17 -0
- skilldrop_cli/data/packs/sre-oncall/skills/postmortem-generator/manifest.json +14 -0
- skilldrop_cli/data/packs/sre-oncall/skills/postmortem-generator/templates/postmortem.md +67 -0
- skilldrop_cli/data/packs/sre-oncall/skills/runbook-generator/SKILL.md +42 -0
- skilldrop_cli/data/packs/sre-oncall/skills/runbook-generator/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/sre-oncall/skills/runbook-generator/evals/evals.json +17 -0
- skilldrop_cli/data/packs/sre-oncall/skills/runbook-generator/manifest.json +11 -0
- skilldrop_cli/data/packs/sre-oncall/skills/runbook-generator/templates/runbook.md +109 -0
- skilldrop_cli/data/packs/stakeholder-comms/pack.json +60 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/audience-profile/SKILL.md +74 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/audience-profile/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/audience-profile/evals/evals.json +15 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/audience-profile/manifest.json +11 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/audience-profile/reference.md +175 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/decision-log/SKILL.md +68 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/decision-log/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/decision-log/evals/evals.json +17 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/decision-log/manifest.json +11 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/exec-summary/SKILL.md +45 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/exec-summary/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/exec-summary/evals/evals.json +17 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/exec-summary/manifest.json +11 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/exec-summary/templates/exec-summary.md +48 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/SKILL.md +77 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/evals/evals.json +18 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/examples/local-setup-guide.md +127 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/manifest.json +11 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/templates/api-reference.md +120 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/templates/design-walkthrough.md +72 -0
- skilldrop_cli/data/packs/stakeholder-comms/skills/guide-builder/templates/setup-guide.md +109 -0
- skilldrop_cli/data/packs/trackers/pack.json +61 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/SKILL.md +116 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/evals/evals.json +30 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/evals/files/1/examples/northwind-backlog.csv +28 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/evals/files/2/issues.json +6052 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/examples/northwind-backlog.csv +28 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/examples/northwind-checkout-triage.md +202 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/manifest.json +42 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/reference.md +106 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/scripts/triage_backlog.py +461 -0
- skilldrop_cli/data/packs/trackers/skills/backlog-triage/templates/triage-report.md +42 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/SKILL.md +111 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/evals/eval_queries.json +30 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/evals/evals.json +30 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/evals/files/1/examples/northwind-week-40.csv +16 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/evals/files/2/issues.json +1212 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/examples/northwind-week-40.csv +16 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/examples/northwind-weekly-status.md +160 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/manifest.json +33 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/reference.md +92 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/scripts/status_counts.py +486 -0
- skilldrop_cli/data/packs/trackers/skills/team-status-report/templates/status-report.md +42 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/SKILL.md +116 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/evals/eval_queries.json +34 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/evals/evals.json +31 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/evals/files/1/examples/prd-guest-checkout.md +53 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/evals/files/2/examples/prd-guest-checkout.md +53 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/evals/files/2/examples/tracker-after-two-weeks.csv +30 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/examples/guest-checkout-spec.json +98 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/examples/guest-checkout-sync.md +151 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/examples/prd-guest-checkout.md +53 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/examples/tracker-after-two-weeks.csv +30 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/manifest.json +41 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/reference.md +101 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/scripts/brief_sync.py +501 -0
- skilldrop_cli/data/packs/trackers/skills/tracker-brief-sync/templates/issues-spec.json +26 -0
- skilldrop_cli/data/profiles.json +24 -0
- skilldrop_cli-0.16.3.dist-info/METADATA +91 -0
- skilldrop_cli-0.16.3.dist-info/RECORD +757 -0
- skilldrop_cli-0.16.3.dist-info/WHEEL +5 -0
- skilldrop_cli-0.16.3.dist-info/entry_points.txt +2 -0
- skilldrop_cli-0.16.3.dist-info/licenses/LICENSE +9 -0
- skilldrop_cli-0.16.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert a document into clean Markdown that another skill can read.
|
|
3
|
+
|
|
4
|
+
Stdlib only, Python 3.9+. Handles .docx, .pptx, .xlsx, .html/.htm, .csv/.tsv,
|
|
5
|
+
.json, .txt/.md, and .pdf (through `pdftotext` when it is installed).
|
|
6
|
+
Everything the conversion could not carry (images, charts, comments, formulas,
|
|
7
|
+
tracked changes) is counted and reported on stderr, so nobody mistakes the
|
|
8
|
+
Markdown for the whole document.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 to_markdown.py report.docx -o report.md
|
|
12
|
+
python3 to_markdown.py deck.pptx -o deck.md
|
|
13
|
+
python3 to_markdown.py model.xlsx -o model.md --max-rows 200
|
|
14
|
+
python3 to_markdown.py page.html -o - # write to stdout
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import csv
|
|
19
|
+
import io
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import zipfile
|
|
26
|
+
from collections import OrderedDict
|
|
27
|
+
from datetime import datetime, timedelta
|
|
28
|
+
from html.parser import HTMLParser
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from xml.etree import ElementTree as ET
|
|
31
|
+
|
|
32
|
+
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
|
33
|
+
R = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
|
|
34
|
+
A = "{http://schemas.openxmlformats.org/drawingml/2006/main}"
|
|
35
|
+
P = "{http://schemas.openxmlformats.org/presentationml/2006/main}"
|
|
36
|
+
S = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
|
|
37
|
+
PKG_REL = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Report:
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.kept = OrderedDict()
|
|
43
|
+
self.dropped = OrderedDict()
|
|
44
|
+
self.notes = []
|
|
45
|
+
|
|
46
|
+
def keep(self, what, n=1):
|
|
47
|
+
self.kept[what] = self.kept.get(what, 0) + n
|
|
48
|
+
|
|
49
|
+
def drop(self, what, n=1):
|
|
50
|
+
if n:
|
|
51
|
+
self.dropped[what] = self.dropped.get(what, 0) + n
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------- helpers
|
|
55
|
+
|
|
56
|
+
def md_escape(text):
|
|
57
|
+
text = text.replace("\\", "\\\\")
|
|
58
|
+
text = re.sub(r"([*_`\[\]])", r"\\\1", text)
|
|
59
|
+
text = re.sub(r"<(?=[A-Za-z/!])", r"\\<", text) # literal <tag> text must not read as raw HTML
|
|
60
|
+
return text
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def escape_line_start(text):
|
|
64
|
+
"""Stop plain text from turning into a heading, list or quote."""
|
|
65
|
+
if re.match(r"^\s*(#{1,6}\s|[-+>]\s|\d+[.)]\s)", text):
|
|
66
|
+
return re.sub(r"^(\s*)([#\-+>]|\d+)", r"\1\\\2", text, count=1)
|
|
67
|
+
return text
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cell_text(text):
|
|
71
|
+
return text.replace("|", "\\|").replace("\n", "<br>").strip()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def md_table(rows):
|
|
75
|
+
rows = [r for r in rows if any(c.strip() for c in r)]
|
|
76
|
+
if not rows:
|
|
77
|
+
return ""
|
|
78
|
+
width = max(len(r) for r in rows)
|
|
79
|
+
rows = [r + [""] * (width - len(r)) for r in rows]
|
|
80
|
+
out = ["| " + " | ".join(cell_text(c) for c in rows[0]) + " |",
|
|
81
|
+
"|" + "|".join(["---"] * width) + "|"]
|
|
82
|
+
for r in rows[1:]:
|
|
83
|
+
out.append("| " + " | ".join(cell_text(c) for c in r) + " |")
|
|
84
|
+
return "\n".join(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def wrap(text, bold, italic):
|
|
88
|
+
if not text.strip():
|
|
89
|
+
return text
|
|
90
|
+
lead = text[:len(text) - len(text.lstrip())]
|
|
91
|
+
trail = text[len(text.rstrip()):]
|
|
92
|
+
core = text.strip()
|
|
93
|
+
if bold and italic:
|
|
94
|
+
core = "***%s***" % core
|
|
95
|
+
elif bold:
|
|
96
|
+
core = "**%s**" % core
|
|
97
|
+
elif italic:
|
|
98
|
+
core = "*%s*" % core
|
|
99
|
+
return lead + core + trail
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render_segments(segs):
|
|
103
|
+
"""segs: list of (text, bold, italic, href). Merge neighbours with equal formatting."""
|
|
104
|
+
merged = []
|
|
105
|
+
for t, b, i, h in segs:
|
|
106
|
+
if merged and merged[-1][1:] == (b, i, h):
|
|
107
|
+
merged[-1] = (merged[-1][0] + t, b, i, h)
|
|
108
|
+
else:
|
|
109
|
+
merged.append((t, b, i, h))
|
|
110
|
+
out = []
|
|
111
|
+
for t, b, i, h in merged:
|
|
112
|
+
piece = wrap(md_escape(t), b, i)
|
|
113
|
+
if h:
|
|
114
|
+
piece = "[%s](%s)" % (piece.strip(), h.replace(" ", "%20").replace(")", "%29"))
|
|
115
|
+
out.append(piece)
|
|
116
|
+
return re.sub(r"[ \t]+", " ", "".join(out)).strip()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def read_rels(z, path):
|
|
120
|
+
rels = {}
|
|
121
|
+
if path in z.namelist():
|
|
122
|
+
for rel in ET.fromstring(z.read(path)).iter(PKG_REL + "Relationship"):
|
|
123
|
+
rels[rel.get("Id")] = (rel.get("Target"), rel.get("Type", ""), rel.get("TargetMode", ""))
|
|
124
|
+
return rels
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve(base_dir, target):
|
|
128
|
+
if target.startswith("/"):
|
|
129
|
+
return target.lstrip("/")
|
|
130
|
+
parts = (base_dir.rstrip("/") + "/" + target).split("/")
|
|
131
|
+
stack = []
|
|
132
|
+
for p in parts:
|
|
133
|
+
if p == "..":
|
|
134
|
+
if stack:
|
|
135
|
+
stack.pop()
|
|
136
|
+
elif p and p != ".":
|
|
137
|
+
stack.append(p)
|
|
138
|
+
return "/".join(stack)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def truthy(el, attr=W + "val"):
|
|
142
|
+
if el is None:
|
|
143
|
+
return False
|
|
144
|
+
v = el.get(attr)
|
|
145
|
+
return v is None or v not in ("0", "false", "off", "none")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def open_zip(path, kind):
|
|
149
|
+
try:
|
|
150
|
+
return zipfile.ZipFile(str(path))
|
|
151
|
+
except zipfile.BadZipFile:
|
|
152
|
+
sys.exit("error: %s is not a valid %s file (not a zip package). If it is an old binary "
|
|
153
|
+
"Office file, save it as %s first." % (path, kind, kind))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------- docx
|
|
157
|
+
|
|
158
|
+
def docx_to_md(path, rep):
|
|
159
|
+
z = open_zip(path, ".docx")
|
|
160
|
+
names = set(z.namelist())
|
|
161
|
+
if "word/document.xml" not in names:
|
|
162
|
+
sys.exit("error: %s has no word/document.xml; it is not a Word document" % path)
|
|
163
|
+
styles = {}
|
|
164
|
+
style_num = {}
|
|
165
|
+
if "word/styles.xml" in names:
|
|
166
|
+
for st in ET.fromstring(z.read("word/styles.xml")).iter(W + "style"):
|
|
167
|
+
sid = st.get(W + "styleId")
|
|
168
|
+
name_el = st.find(W + "name")
|
|
169
|
+
name = (name_el.get(W + "val") if name_el is not None else sid or "").lower()
|
|
170
|
+
ppr = st.find(W + "pPr")
|
|
171
|
+
outline = None
|
|
172
|
+
if ppr is not None:
|
|
173
|
+
ol = ppr.find(W + "outlineLvl")
|
|
174
|
+
if ol is not None:
|
|
175
|
+
outline = int(ol.get(W + "val", "9"))
|
|
176
|
+
num = ppr.find(W + "numPr")
|
|
177
|
+
if num is not None and num.find(W + "numId") is not None:
|
|
178
|
+
style_num[sid] = num.find(W + "numId").get(W + "val")
|
|
179
|
+
styles[sid] = (name, outline)
|
|
180
|
+
num_fmt = {}
|
|
181
|
+
if "word/numbering.xml" in names:
|
|
182
|
+
root = ET.fromstring(z.read("word/numbering.xml"))
|
|
183
|
+
abstract = {}
|
|
184
|
+
for an in root.iter(W + "abstractNum"):
|
|
185
|
+
lv = {}
|
|
186
|
+
for lvl in an.iter(W + "lvl"):
|
|
187
|
+
f = lvl.find(W + "numFmt")
|
|
188
|
+
lv[lvl.get(W + "ilvl")] = f.get(W + "val") if f is not None else "bullet"
|
|
189
|
+
abstract[an.get(W + "abstractNumId")] = lv
|
|
190
|
+
for num in root.iter(W + "num"):
|
|
191
|
+
ref = num.find(W + "abstractNumId")
|
|
192
|
+
if ref is not None:
|
|
193
|
+
num_fmt[num.get(W + "numId")] = abstract.get(ref.get(W + "val"), {})
|
|
194
|
+
rels = read_rels(z, "word/_rels/document.xml.rels")
|
|
195
|
+
|
|
196
|
+
if "word/comments.xml" in names:
|
|
197
|
+
rep.drop("comments", len(list(ET.fromstring(z.read("word/comments.xml")).iter(W + "comment"))))
|
|
198
|
+
if "word/footnotes.xml" in names:
|
|
199
|
+
fns = [f for f in ET.fromstring(z.read("word/footnotes.xml")).iter(W + "footnote")
|
|
200
|
+
if int(f.get(W + "id", "0")) > 0]
|
|
201
|
+
rep.drop("footnotes", len(fns))
|
|
202
|
+
rep.drop("charts", len([n for n in names if re.match(r"word/charts/chart\d+\.xml$", n)]))
|
|
203
|
+
for n in names:
|
|
204
|
+
if re.match(r"word/(header|footer)\d*\.xml$", n):
|
|
205
|
+
if "".join(t.text or "" for t in ET.fromstring(z.read(n)).iter(W + "t")).strip():
|
|
206
|
+
rep.drop("headers/footers")
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
body = ET.fromstring(z.read("word/document.xml")).find(W + "body")
|
|
210
|
+
|
|
211
|
+
def para_segments(p, in_table=False):
|
|
212
|
+
segs = []
|
|
213
|
+
field = {"instr": "", "href": None, "state": None}
|
|
214
|
+
|
|
215
|
+
def run(r, href):
|
|
216
|
+
rpr = r.find(W + "rPr")
|
|
217
|
+
b = rpr is not None and truthy(rpr.find(W + "b"))
|
|
218
|
+
i = rpr is not None and truthy(rpr.find(W + "i"))
|
|
219
|
+
for ch in r:
|
|
220
|
+
tag = ch.tag
|
|
221
|
+
if tag == W + "fldChar":
|
|
222
|
+
kind = ch.get(W + "fldCharType")
|
|
223
|
+
if kind == "begin":
|
|
224
|
+
field.update(instr="", href=None, state="instr")
|
|
225
|
+
elif kind == "separate":
|
|
226
|
+
m = re.search(r'HYPERLINK\s+"([^"]+)"', field["instr"])
|
|
227
|
+
field["href"] = m.group(1) if m else None
|
|
228
|
+
field["state"] = "result"
|
|
229
|
+
elif kind == "end":
|
|
230
|
+
field.update(state=None, href=None)
|
|
231
|
+
elif tag == W + "instrText":
|
|
232
|
+
field["instr"] += ch.text or ""
|
|
233
|
+
elif field["state"] == "instr":
|
|
234
|
+
continue
|
|
235
|
+
elif tag == W + "t":
|
|
236
|
+
segs.append((ch.text or "", b, i, href or field["href"]))
|
|
237
|
+
elif tag == W + "tab":
|
|
238
|
+
segs.append((" ", False, False, None))
|
|
239
|
+
elif tag in (W + "br", W + "cr"):
|
|
240
|
+
segs.append(("\n", False, False, None))
|
|
241
|
+
elif tag in (W + "pict", W + "object"):
|
|
242
|
+
rep.drop("images")
|
|
243
|
+
elif tag == W + "drawing":
|
|
244
|
+
uris = [g.get("uri") or "" for g in ch.iter(A + "graphicData")]
|
|
245
|
+
if any(True for _ in ch.iter(A + "blip")):
|
|
246
|
+
rep.drop("images")
|
|
247
|
+
elif not any("chart" in u for u in uris): # charts are counted from word/charts
|
|
248
|
+
rep.drop("shapes/text boxes")
|
|
249
|
+
|
|
250
|
+
def walk(node, href):
|
|
251
|
+
for ch in node:
|
|
252
|
+
if ch.tag == W + "r":
|
|
253
|
+
run(ch, href)
|
|
254
|
+
elif ch.tag == W + "hyperlink":
|
|
255
|
+
rid = ch.get(R + "id")
|
|
256
|
+
target = rels.get(rid, (None,))[0] if rid else None
|
|
257
|
+
walk(ch, target)
|
|
258
|
+
elif ch.tag == W + "ins":
|
|
259
|
+
rep.drop("tracked insertions (kept as text)")
|
|
260
|
+
walk(ch, href)
|
|
261
|
+
elif ch.tag == W + "del":
|
|
262
|
+
rep.drop("tracked deletions (left out)")
|
|
263
|
+
elif ch.tag in (W + "smartTag", W + "sdt", W + "sdtContent", W + "fldSimple"):
|
|
264
|
+
walk(ch, href)
|
|
265
|
+
walk(p, None)
|
|
266
|
+
text = render_segments(segs)
|
|
267
|
+
return text.replace("\n", "<br>" if in_table else " \n")
|
|
268
|
+
|
|
269
|
+
def heading_level(p):
|
|
270
|
+
ppr = p.find(W + "pPr")
|
|
271
|
+
if ppr is None:
|
|
272
|
+
return None
|
|
273
|
+
ol = ppr.find(W + "outlineLvl")
|
|
274
|
+
if ol is not None and int(ol.get(W + "val", "9")) < 6:
|
|
275
|
+
return int(ol.get(W + "val")) + 1
|
|
276
|
+
ps = ppr.find(W + "pStyle")
|
|
277
|
+
if ps is None:
|
|
278
|
+
return None
|
|
279
|
+
name, outline = styles.get(ps.get(W + "val"), (ps.get(W + "val", "").lower(), None))
|
|
280
|
+
if name == "title":
|
|
281
|
+
return 1
|
|
282
|
+
m = re.match(r"heading\s*(\d)", name)
|
|
283
|
+
if m:
|
|
284
|
+
return min(int(m.group(1)), 6)
|
|
285
|
+
if outline is not None and outline < 6:
|
|
286
|
+
return outline + 1
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def list_info(p):
|
|
290
|
+
ppr = p.find(W + "pPr")
|
|
291
|
+
if ppr is None:
|
|
292
|
+
return None
|
|
293
|
+
num = ppr.find(W + "numPr")
|
|
294
|
+
num_id, ilvl = None, "0"
|
|
295
|
+
if num is not None:
|
|
296
|
+
nid = num.find(W + "numId")
|
|
297
|
+
lvl = num.find(W + "ilvl")
|
|
298
|
+
num_id = nid.get(W + "val") if nid is not None else None
|
|
299
|
+
ilvl = lvl.get(W + "val") if lvl is not None else "0"
|
|
300
|
+
ps = ppr.find(W + "pStyle")
|
|
301
|
+
sid = ps.get(W + "val") if ps is not None else None
|
|
302
|
+
if num_id is None and sid in style_num:
|
|
303
|
+
num_id = style_num[sid]
|
|
304
|
+
if num_id is None or num_id == "0":
|
|
305
|
+
name = styles.get(sid, ("", None))[0] if sid else ""
|
|
306
|
+
if "list bullet" in name:
|
|
307
|
+
return ("bullet", 0)
|
|
308
|
+
if "list number" in name:
|
|
309
|
+
return ("decimal", 0)
|
|
310
|
+
return None
|
|
311
|
+
fmt = num_fmt.get(num_id, {}).get(ilvl, "bullet")
|
|
312
|
+
return (fmt, int(ilvl))
|
|
313
|
+
|
|
314
|
+
def table_md(tbl):
|
|
315
|
+
rows = []
|
|
316
|
+
for tr in tbl.findall(W + "tr"):
|
|
317
|
+
row = []
|
|
318
|
+
for tc in tr.findall(W + "tc"):
|
|
319
|
+
tcpr = tc.find(W + "tcPr")
|
|
320
|
+
parts = []
|
|
321
|
+
for p in tc.iter(W + "p"):
|
|
322
|
+
t = para_segments(p, in_table=True)
|
|
323
|
+
if t:
|
|
324
|
+
parts.append(t)
|
|
325
|
+
text = "<br>".join(parts)
|
|
326
|
+
if tcpr is not None:
|
|
327
|
+
vm = tcpr.find(W + "vMerge")
|
|
328
|
+
if vm is not None and vm.get(W + "val") in (None, "continue"):
|
|
329
|
+
text = ""
|
|
330
|
+
row.append(text)
|
|
331
|
+
if tcpr is not None and tcpr.find(W + "gridSpan") is not None:
|
|
332
|
+
row.extend([""] * (int(tcpr.find(W + "gridSpan").get(W + "val", "1")) - 1))
|
|
333
|
+
rows.append(row)
|
|
334
|
+
if len(list(tbl.iter(W + "tbl"))) > 1:
|
|
335
|
+
rep.notes.append("a nested table was flattened into its parent cell")
|
|
336
|
+
rep.keep("tables")
|
|
337
|
+
return md_table(rows)
|
|
338
|
+
|
|
339
|
+
def raw_text(p):
|
|
340
|
+
return "".join((n.text or "") if n.tag == W + "t" else "\t" for n in p.iter()
|
|
341
|
+
if n.tag in (W + "t", W + "tab"))
|
|
342
|
+
|
|
343
|
+
def style_name(p):
|
|
344
|
+
ppr = p.find(W + "pPr")
|
|
345
|
+
ps = ppr.find(W + "pStyle") if ppr is not None else None
|
|
346
|
+
if ps is None:
|
|
347
|
+
return ""
|
|
348
|
+
sid = ps.get(W + "val", "")
|
|
349
|
+
return styles.get(sid, (sid.lower(), None))[0]
|
|
350
|
+
|
|
351
|
+
def code_text(p):
|
|
352
|
+
"""Text of a code paragraph: line breaks and tabs kept, no inline formatting or escaping."""
|
|
353
|
+
return "".join((n.text or "") if n.tag == W + "t" else "\t" if n.tag == W + "tab" else "\n"
|
|
354
|
+
for n in p.iter() if n.tag in (W + "t", W + "tab", W + "br", W + "cr"))
|
|
355
|
+
|
|
356
|
+
def run_size(p):
|
|
357
|
+
"""(largest font size in half-points, every text run bold?) for unstyled documents."""
|
|
358
|
+
sizes, bold = [0], True
|
|
359
|
+
for r in p.iter(W + "r"):
|
|
360
|
+
if not "".join(t.text or "" for t in r.iter(W + "t")).strip():
|
|
361
|
+
continue
|
|
362
|
+
rpr = r.find(W + "rPr")
|
|
363
|
+
sz = rpr.find(W + "sz") if rpr is not None else None
|
|
364
|
+
sizes.append(int(sz.get(W + "val", "0")) if sz is not None else 0)
|
|
365
|
+
bold = bold and rpr is not None and truthy(rpr.find(W + "b"))
|
|
366
|
+
return max(sizes), bold
|
|
367
|
+
|
|
368
|
+
# Documents saved without heading styles (pasted text, textutil, some exporters) mark
|
|
369
|
+
# headings with size and bold only. Infer levels from that, and say so.
|
|
370
|
+
inferred = {}
|
|
371
|
+
top = [el for el in body if el.tag == W + "p"]
|
|
372
|
+
if not any(heading_level(p) for p in top):
|
|
373
|
+
weight = {}
|
|
374
|
+
info = {}
|
|
375
|
+
for p in top:
|
|
376
|
+
t = raw_text(p).strip()
|
|
377
|
+
if not t:
|
|
378
|
+
continue
|
|
379
|
+
sz, b = run_size(p)
|
|
380
|
+
info[id(p)] = (sz, b, t)
|
|
381
|
+
weight[sz] = weight.get(sz, 0) + len(t)
|
|
382
|
+
if weight:
|
|
383
|
+
body_sz = max(weight, key=weight.get)
|
|
384
|
+
bigger = sorted({sz for sz, b, t in info.values() if sz > body_sz and len(t) <= 120}, reverse=True)
|
|
385
|
+
for p in top:
|
|
386
|
+
if id(p) not in info:
|
|
387
|
+
continue
|
|
388
|
+
sz, b, t = info[id(p)]
|
|
389
|
+
if len(t) > 120 or t.endswith((".", ":", ";")) and sz <= body_sz:
|
|
390
|
+
continue
|
|
391
|
+
if sz in bigger: # bold alone is not enough: bold table cells and labels look the same
|
|
392
|
+
inferred[id(p)] = min(bigger.index(sz) + 1, 6)
|
|
393
|
+
if inferred:
|
|
394
|
+
rep.notes.append("no heading styles in the document; %d heading(s) inferred from font "
|
|
395
|
+
"size and bold, so check the levels" % len(inferred))
|
|
396
|
+
|
|
397
|
+
blocks = []
|
|
398
|
+
counters = {}
|
|
399
|
+
prev_list = False
|
|
400
|
+
for el in body:
|
|
401
|
+
if el.tag == W + "p":
|
|
402
|
+
text = para_segments(el)
|
|
403
|
+
lvl = heading_level(el) or inferred.get(id(el))
|
|
404
|
+
li = list_info(el)
|
|
405
|
+
if not li and not lvl:
|
|
406
|
+
raw = raw_text(el)
|
|
407
|
+
m = re.match(r"^\s*([\u2022\u25e6\u25aa\u2023\u00b7\u25cf\u25cb\u25a0\u2013])\s*", raw)
|
|
408
|
+
m2 = re.match(r"^\s*(\d{1,3})[.)]?\t", raw)
|
|
409
|
+
if m or m2:
|
|
410
|
+
li = ("bullet", 0) if m else ("decimal", 0)
|
|
411
|
+
text = re.sub(r"^\s*(\S{1,4})\s+", "", text, count=1)
|
|
412
|
+
rep.notes.append("list items written as typed bullets or numbers were turned into a Markdown list") \
|
|
413
|
+
if not any("typed bullets" in n for n in rep.notes) else None
|
|
414
|
+
if lvl and id(el) in inferred:
|
|
415
|
+
text = re.sub(r"^\*{1,3}(.*?)\*{1,3}$", r"\1", text)
|
|
416
|
+
if not text:
|
|
417
|
+
prev_list = False if not li else prev_list
|
|
418
|
+
continue
|
|
419
|
+
sname = "" if lvl or li else style_name(el)
|
|
420
|
+
if re.match(r"(source ?code|code|html preformatted|preformatted text|macro text)$", sname):
|
|
421
|
+
# one code block may be one paragraph with line breaks or a run of one-line paragraphs
|
|
422
|
+
if blocks and blocks[-1][0] == "code":
|
|
423
|
+
blocks[-1] = ("code", blocks[-1][1] + "\n" + code_text(el))
|
|
424
|
+
else:
|
|
425
|
+
blocks.append(("code", code_text(el)))
|
|
426
|
+
rep.keep("code blocks")
|
|
427
|
+
prev_list = False
|
|
428
|
+
elif "quote" in sname or sname == "block text":
|
|
429
|
+
blocks.append(("block", "> " + text.replace("\n", "\n> ")))
|
|
430
|
+
prev_list = False
|
|
431
|
+
elif lvl:
|
|
432
|
+
rep.keep("headings")
|
|
433
|
+
blocks.append(("block", "#" * lvl + " " + text.replace(" \n", " ")))
|
|
434
|
+
prev_list = False
|
|
435
|
+
counters = {}
|
|
436
|
+
elif li:
|
|
437
|
+
fmt, depth = li
|
|
438
|
+
if not prev_list:
|
|
439
|
+
counters = {}
|
|
440
|
+
for d in list(counters):
|
|
441
|
+
if d > depth:
|
|
442
|
+
del counters[d]
|
|
443
|
+
kind = "ul" if fmt in ("bullet", "none") else "ol"
|
|
444
|
+
prev_kind, count = counters.get(depth, (kind, 0))
|
|
445
|
+
count = count + 1 if prev_kind == kind else 1
|
|
446
|
+
counters[depth] = (kind, count)
|
|
447
|
+
marker = "-" if kind == "ul" else "%d." % count
|
|
448
|
+
blocks.append(("list", " " * depth + marker + " " + text))
|
|
449
|
+
rep.keep("list items")
|
|
450
|
+
prev_list = True
|
|
451
|
+
else:
|
|
452
|
+
blocks.append(("block", escape_line_start(text)))
|
|
453
|
+
prev_list = False
|
|
454
|
+
elif el.tag == W + "tbl":
|
|
455
|
+
blocks.append(("block", table_md(el)))
|
|
456
|
+
prev_list = False
|
|
457
|
+
elif el.tag == W + "sdt":
|
|
458
|
+
content = el.find(W + "sdtContent")
|
|
459
|
+
if content is not None:
|
|
460
|
+
for p in content.iter(W + "p"):
|
|
461
|
+
t = para_segments(p)
|
|
462
|
+
if t:
|
|
463
|
+
blocks.append(("block", t))
|
|
464
|
+
out = []
|
|
465
|
+
for k, (kind, text) in enumerate(blocks):
|
|
466
|
+
if k and not (kind == "list" and blocks[k - 1][0] == "list"):
|
|
467
|
+
out.append("")
|
|
468
|
+
if kind == "code":
|
|
469
|
+
fence = "```"
|
|
470
|
+
while fence in text:
|
|
471
|
+
fence += "`"
|
|
472
|
+
text = fence + "\n" + text + "\n" + fence
|
|
473
|
+
out.append(text)
|
|
474
|
+
return "\n".join(out) + "\n"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ---------------------------------------------------------------- pptx
|
|
478
|
+
|
|
479
|
+
def pptx_to_md(path, rep):
|
|
480
|
+
z = open_zip(path, ".pptx")
|
|
481
|
+
names = set(z.namelist())
|
|
482
|
+
if "ppt/presentation.xml" not in names:
|
|
483
|
+
sys.exit("error: %s has no ppt/presentation.xml; it is not a PowerPoint file" % path)
|
|
484
|
+
pres = ET.fromstring(z.read("ppt/presentation.xml"))
|
|
485
|
+
prels = read_rels(z, "ppt/_rels/presentation.xml.rels")
|
|
486
|
+
slide_paths = []
|
|
487
|
+
lst = pres.find(P + "sldIdLst")
|
|
488
|
+
for sid in (lst if lst is not None else []):
|
|
489
|
+
target = prels.get(sid.get(R + "id"), (None,))[0]
|
|
490
|
+
if target:
|
|
491
|
+
slide_paths.append(resolve("ppt", target))
|
|
492
|
+
rep.drop("comments", len([n for n in names if re.match(r"ppt/comments/.*\.xml$", n)]))
|
|
493
|
+
|
|
494
|
+
def para_text(p, rels):
|
|
495
|
+
segs = []
|
|
496
|
+
for ch in p:
|
|
497
|
+
if ch.tag in (A + "r", A + "fld"):
|
|
498
|
+
rpr = ch.find(A + "rPr")
|
|
499
|
+
b = rpr is not None and rpr.get("b") in ("1", "true")
|
|
500
|
+
i = rpr is not None and rpr.get("i") in ("1", "true")
|
|
501
|
+
href = None
|
|
502
|
+
if rpr is not None and rpr.find(A + "hlinkClick") is not None:
|
|
503
|
+
rid = rpr.find(A + "hlinkClick").get(R + "id")
|
|
504
|
+
tgt = rels.get(rid)
|
|
505
|
+
if tgt and tgt[2] == "External":
|
|
506
|
+
href = tgt[0]
|
|
507
|
+
t = ch.find(A + "t")
|
|
508
|
+
segs.append((t.text or "" if t is not None else "", b, i, href))
|
|
509
|
+
elif ch.tag == A + "br":
|
|
510
|
+
segs.append((" ", False, False, None))
|
|
511
|
+
return render_segments(segs)
|
|
512
|
+
|
|
513
|
+
def shape_lines(sp, rels, is_body):
|
|
514
|
+
lines = []
|
|
515
|
+
tx = sp.find(P + "txBody")
|
|
516
|
+
if tx is None:
|
|
517
|
+
return lines
|
|
518
|
+
for p in tx.findall(A + "p"):
|
|
519
|
+
text = para_text(p, rels)
|
|
520
|
+
if not text:
|
|
521
|
+
continue
|
|
522
|
+
ppr = p.find(A + "pPr")
|
|
523
|
+
lvl = int(ppr.get("lvl", "0")) if ppr is not None else 0
|
|
524
|
+
bullet = is_body
|
|
525
|
+
if ppr is not None:
|
|
526
|
+
if ppr.find(A + "buNone") is not None:
|
|
527
|
+
bullet = False
|
|
528
|
+
elif ppr.find(A + "buChar") is not None or ppr.find(A + "buAutoNum") is not None:
|
|
529
|
+
bullet = True
|
|
530
|
+
lines.append((" " * lvl + "- " + text) if bullet else escape_line_start(text))
|
|
531
|
+
return lines
|
|
532
|
+
|
|
533
|
+
def walk_tree(tree, rels, title_box, lines):
|
|
534
|
+
for sp in tree:
|
|
535
|
+
tag = sp.tag
|
|
536
|
+
if tag == P + "sp":
|
|
537
|
+
ph = sp.find(P + "nvSpPr/" + P + "nvPr/" + P + "ph")
|
|
538
|
+
ph_type = ph.get("type", "body") if ph is not None else None
|
|
539
|
+
if ph_type in ("title", "ctrTitle"):
|
|
540
|
+
title_box.append(" ".join(para_text(p, rels) for p in sp.iter(A + "p")).strip())
|
|
541
|
+
continue
|
|
542
|
+
if ph_type in ("sldNum", "dt", "ftr"):
|
|
543
|
+
continue
|
|
544
|
+
got = shape_lines(sp, rels, ph_type in ("body", "obj"))
|
|
545
|
+
if got:
|
|
546
|
+
if lines:
|
|
547
|
+
lines.append("")
|
|
548
|
+
lines.extend(got)
|
|
549
|
+
elif tag == P + "pic":
|
|
550
|
+
rep.drop("images")
|
|
551
|
+
elif tag == P + "graphicFrame":
|
|
552
|
+
tbl = sp.find(".//" + A + "tbl")
|
|
553
|
+
if tbl is not None:
|
|
554
|
+
rows = []
|
|
555
|
+
for tr in tbl.findall(A + "tr"):
|
|
556
|
+
rows.append([" ".join(para_text(p, rels) for p in tc.iter(A + "p")).strip()
|
|
557
|
+
for tc in tr.findall(A + "tc")])
|
|
558
|
+
lines.extend(["", md_table(rows)])
|
|
559
|
+
rep.keep("tables")
|
|
560
|
+
else:
|
|
561
|
+
gd = sp.find(".//" + A + "graphicData")
|
|
562
|
+
uri = gd.get("uri", "") if gd is not None else ""
|
|
563
|
+
rep.drop("charts" if "chart" in uri else "diagrams/objects")
|
|
564
|
+
elif tag == P + "grpSp":
|
|
565
|
+
walk_tree(sp, rels, title_box, lines)
|
|
566
|
+
elif tag == P + "cxnSp":
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
out = []
|
|
570
|
+
for n, sp_path in enumerate(slide_paths, 1):
|
|
571
|
+
if sp_path not in names:
|
|
572
|
+
rep.notes.append("slide %d is listed but missing from the file" % n)
|
|
573
|
+
continue
|
|
574
|
+
root = ET.fromstring(z.read(sp_path))
|
|
575
|
+
base = sp_path.rsplit("/", 1)[0]
|
|
576
|
+
rels_path = base + "/_rels/" + sp_path.rsplit("/", 1)[1] + ".rels"
|
|
577
|
+
rels = read_rels(z, rels_path)
|
|
578
|
+
tree = root.find(P + "cSld/" + P + "spTree")
|
|
579
|
+
title_box, lines = [], []
|
|
580
|
+
if tree is not None:
|
|
581
|
+
walk_tree(tree, rels, title_box, lines)
|
|
582
|
+
title = title_box[0] if title_box and title_box[0] else "(untitled)"
|
|
583
|
+
hidden = " (hidden)" if root.get("show") == "0" else ""
|
|
584
|
+
out.append("## Slide %d: %s%s" % (n, title, hidden))
|
|
585
|
+
rep.keep("slides")
|
|
586
|
+
if lines:
|
|
587
|
+
out.append("")
|
|
588
|
+
out.extend(lines)
|
|
589
|
+
for tgt, typ, _ in rels.values():
|
|
590
|
+
if typ.endswith("/notesSlide"):
|
|
591
|
+
npath = resolve(base, tgt)
|
|
592
|
+
if npath in names:
|
|
593
|
+
nroot = ET.fromstring(z.read(npath))
|
|
594
|
+
notes = []
|
|
595
|
+
for sp in nroot.iter(P + "sp"):
|
|
596
|
+
ph = sp.find(P + "nvSpPr/" + P + "nvPr/" + P + "ph")
|
|
597
|
+
if ph is not None and ph.get("type") == "body":
|
|
598
|
+
notes += [para_text(p, rels) for p in sp.iter(A + "p")]
|
|
599
|
+
notes = [t for t in notes if t]
|
|
600
|
+
if notes:
|
|
601
|
+
rep.keep("slides with speaker notes")
|
|
602
|
+
out.append("")
|
|
603
|
+
out.append("> **Speaker notes:** " + notes[0])
|
|
604
|
+
for t in notes[1:]:
|
|
605
|
+
out.append(">")
|
|
606
|
+
out.append("> " + t)
|
|
607
|
+
out.append("")
|
|
608
|
+
if not slide_paths:
|
|
609
|
+
rep.notes.append("the presentation has no slides")
|
|
610
|
+
return "\n".join(out).rstrip() + "\n"
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ---------------------------------------------------------------- xlsx
|
|
614
|
+
|
|
615
|
+
BUILTIN_DATE_FMTS = set(range(14, 23)) | {45, 46, 47}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def col_index(ref):
|
|
619
|
+
letters = re.match(r"([A-Z]+)", ref).group(1)
|
|
620
|
+
n = 0
|
|
621
|
+
for ch in letters:
|
|
622
|
+
n = n * 26 + (ord(ch) - 64)
|
|
623
|
+
return n - 1
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def xlsx_to_md(path, rep, max_rows=0):
|
|
627
|
+
z = open_zip(path, ".xlsx")
|
|
628
|
+
names = set(z.namelist())
|
|
629
|
+
if "xl/workbook.xml" not in names:
|
|
630
|
+
sys.exit("error: %s has no xl/workbook.xml; it is not an Excel workbook" % path)
|
|
631
|
+
wb = ET.fromstring(z.read("xl/workbook.xml"))
|
|
632
|
+
pr = wb.find(S + "workbookPr")
|
|
633
|
+
epoch = datetime(1904, 1, 1) if (pr is not None and pr.get("date1904") in ("1", "true")) \
|
|
634
|
+
else datetime(1899, 12, 30)
|
|
635
|
+
shared = []
|
|
636
|
+
if "xl/sharedStrings.xml" in names:
|
|
637
|
+
for si in ET.fromstring(z.read("xl/sharedStrings.xml")).findall(S + "si"):
|
|
638
|
+
# plain <t>, or rich-text runs <r><t>; phonetic hints <rPh> are skipped
|
|
639
|
+
parts = [si.find(S + "t")] + [r.find(S + "t") for r in si.findall(S + "r")]
|
|
640
|
+
shared.append("".join(t.text or "" for t in parts if t is not None))
|
|
641
|
+
date_styles = set()
|
|
642
|
+
if "xl/styles.xml" in names:
|
|
643
|
+
st = ET.fromstring(z.read("xl/styles.xml"))
|
|
644
|
+
custom = {}
|
|
645
|
+
nf = st.find(S + "numFmts")
|
|
646
|
+
for f in (nf if nf is not None else []):
|
|
647
|
+
code = re.sub(r'"[^"]*"|\[[^\]]*\]|\\.', "", f.get("formatCode", "")).lower()
|
|
648
|
+
custom[int(f.get("numFmtId"))] = bool(re.search(r"[dmyh]", code))
|
|
649
|
+
xfs = st.find(S + "cellXfs")
|
|
650
|
+
for k, xf in enumerate(xfs if xfs is not None else []):
|
|
651
|
+
fid = int(xf.get("numFmtId", "0"))
|
|
652
|
+
if fid in BUILTIN_DATE_FMTS or custom.get(fid):
|
|
653
|
+
date_styles.add(k)
|
|
654
|
+
rels = read_rels(z, "xl/_rels/workbook.xml.rels")
|
|
655
|
+
rep.drop("charts", len([n for n in names if re.match(r"xl/charts/chart\d+\.xml$", n)]))
|
|
656
|
+
rep.drop("images", len([n for n in names if n.startswith("xl/media/")]))
|
|
657
|
+
for n in names:
|
|
658
|
+
if re.match(r"xl/comments\d*\.xml$", n):
|
|
659
|
+
rep.drop("comments", len(list(ET.fromstring(z.read(n)).iter(S + "comment"))))
|
|
660
|
+
|
|
661
|
+
out = []
|
|
662
|
+
sheets = wb.find(S + "sheets")
|
|
663
|
+
for sh in (sheets if sheets is not None else []):
|
|
664
|
+
name = sh.get("name")
|
|
665
|
+
target = rels.get(sh.get(R + "id"), (None,))[0]
|
|
666
|
+
if not target:
|
|
667
|
+
continue
|
|
668
|
+
spath = resolve("xl", target)
|
|
669
|
+
if spath not in names:
|
|
670
|
+
continue
|
|
671
|
+
root = ET.fromstring(z.read(spath))
|
|
672
|
+
grid = {}
|
|
673
|
+
formulas = 0
|
|
674
|
+
for c in root.iter(S + "c"):
|
|
675
|
+
ref = c.get("r")
|
|
676
|
+
if not ref:
|
|
677
|
+
continue
|
|
678
|
+
row = int(re.search(r"(\d+)$", ref).group(1)) - 1
|
|
679
|
+
col = col_index(ref)
|
|
680
|
+
t = c.get("t", "n")
|
|
681
|
+
v = c.find(S + "v")
|
|
682
|
+
if c.find(S + "f") is not None:
|
|
683
|
+
formulas += 1
|
|
684
|
+
if t == "s" and v is not None:
|
|
685
|
+
val = shared[int(v.text)] if int(v.text) < len(shared) else ""
|
|
686
|
+
elif t == "inlineStr":
|
|
687
|
+
val = "".join(x.text or "" for x in c.iter(S + "t"))
|
|
688
|
+
elif t == "b" and v is not None:
|
|
689
|
+
val = "TRUE" if v.text == "1" else "FALSE"
|
|
690
|
+
elif v is not None and v.text is not None:
|
|
691
|
+
val = v.text
|
|
692
|
+
if t == "n" and int(c.get("s", "0")) in date_styles:
|
|
693
|
+
try:
|
|
694
|
+
d = epoch + timedelta(days=float(val))
|
|
695
|
+
val = d.strftime("%Y-%m-%d") if float(val) == int(float(val)) \
|
|
696
|
+
else d.strftime("%Y-%m-%d %H:%M")
|
|
697
|
+
except (ValueError, OverflowError):
|
|
698
|
+
pass
|
|
699
|
+
else:
|
|
700
|
+
continue
|
|
701
|
+
if val != "":
|
|
702
|
+
grid[(row, col)] = val
|
|
703
|
+
state = sh.get("state")
|
|
704
|
+
label = "## %s%s" % (name, " (%s)" % state if state in ("hidden", "veryHidden") else "")
|
|
705
|
+
out.append(label)
|
|
706
|
+
out.append("")
|
|
707
|
+
merges = root.find(S + "mergeCells")
|
|
708
|
+
if merges is not None and len(merges):
|
|
709
|
+
rep.notes.append("sheet '%s' has %d merged range(s); the value sits in the first cell only"
|
|
710
|
+
% (name, len(merges)))
|
|
711
|
+
if formulas:
|
|
712
|
+
rep.drop("formulas (cached values kept)", formulas)
|
|
713
|
+
if not grid:
|
|
714
|
+
out.append("*(empty sheet)*")
|
|
715
|
+
out.append("")
|
|
716
|
+
continue
|
|
717
|
+
rows_used = sorted({r for r, _ in grid})
|
|
718
|
+
cols_used = sorted({c for _, c in grid})
|
|
719
|
+
c0, c1 = cols_used[0], cols_used[-1]
|
|
720
|
+
table = []
|
|
721
|
+
for r in range(rows_used[0], rows_used[-1] + 1):
|
|
722
|
+
table.append([grid.get((r, c), "") for c in range(c0, c1 + 1)])
|
|
723
|
+
table = [r for r in table if any(r)]
|
|
724
|
+
if max_rows and len(table) > max_rows + 1:
|
|
725
|
+
rep.notes.append("sheet '%s' cut to %d of %d data rows (--max-rows)"
|
|
726
|
+
% (name, max_rows, len(table) - 1))
|
|
727
|
+
table = table[:max_rows + 1]
|
|
728
|
+
out.append(md_table(table))
|
|
729
|
+
out.append("")
|
|
730
|
+
rep.keep("sheets")
|
|
731
|
+
return "\n".join(out).rstrip() + "\n"
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# ---------------------------------------------------------------- html
|
|
735
|
+
|
|
736
|
+
class HTMLToMD(HTMLParser):
|
|
737
|
+
SKIP = {"script", "style", "noscript", "template", "svg", "head", "iframe", "object", "canvas", "nav"}
|
|
738
|
+
BLOCK = {"p", "div", "section", "article", "header", "footer", "main", "aside", "nav",
|
|
739
|
+
"figure", "figcaption", "address", "form", "fieldset", "dl", "dt", "dd", "hr"}
|
|
740
|
+
|
|
741
|
+
INLINE = {"strong": "**", "b": "**", "em": "*", "i": "*", "code": "`"}
|
|
742
|
+
|
|
743
|
+
def __init__(self, rep):
|
|
744
|
+
HTMLParser.__init__(self, convert_charrefs=True)
|
|
745
|
+
self.inline_stack = []
|
|
746
|
+
self.in_code = 0
|
|
747
|
+
self.rep = rep
|
|
748
|
+
self.lines = []
|
|
749
|
+
self.buf = []
|
|
750
|
+
self.skip = 0
|
|
751
|
+
self.lists = [] # stack of ["ul"|"ol", counter]
|
|
752
|
+
self.quote = 0
|
|
753
|
+
self.pre = False
|
|
754
|
+
self.pre_buf = []
|
|
755
|
+
self.pre_lang = ""
|
|
756
|
+
self.links = []
|
|
757
|
+
self.table = None # list of rows
|
|
758
|
+
self.cell = None
|
|
759
|
+
self.heading = 0
|
|
760
|
+
self.li_prefix = None
|
|
761
|
+
|
|
762
|
+
# output helpers
|
|
763
|
+
def text_target(self):
|
|
764
|
+
return self.cell if self.cell is not None else self.buf
|
|
765
|
+
|
|
766
|
+
def flush(self):
|
|
767
|
+
text = re.sub(r"[ \t\r\n]+", " ", "".join(self.buf)).strip()
|
|
768
|
+
self.buf = []
|
|
769
|
+
if not text and self.li_prefix is None:
|
|
770
|
+
return
|
|
771
|
+
if self.heading:
|
|
772
|
+
line = "#" * self.heading + " " + text
|
|
773
|
+
elif self.li_prefix is not None:
|
|
774
|
+
line = self.li_prefix + text
|
|
775
|
+
self.li_prefix = None
|
|
776
|
+
else:
|
|
777
|
+
line = escape_line_start(text)
|
|
778
|
+
if self.quote:
|
|
779
|
+
line = "> " * self.quote + line
|
|
780
|
+
item = re.compile(r"^(> )*\s*(-|\d+\.) ")
|
|
781
|
+
both_items = self.lines and item.match(self.lines[-1]) and item.match(line)
|
|
782
|
+
if self.lines and self.lines[-1] != "" and not both_items:
|
|
783
|
+
self.lines.append("")
|
|
784
|
+
self.lines.append(line)
|
|
785
|
+
|
|
786
|
+
def blank(self):
|
|
787
|
+
if self.lines and self.lines[-1] != "":
|
|
788
|
+
self.lines.append("")
|
|
789
|
+
|
|
790
|
+
def handle_starttag(self, tag, attrs):
|
|
791
|
+
a = dict(attrs)
|
|
792
|
+
if tag in self.SKIP:
|
|
793
|
+
self.skip += 1
|
|
794
|
+
return
|
|
795
|
+
if self.skip:
|
|
796
|
+
return
|
|
797
|
+
if self.pre:
|
|
798
|
+
if tag == "code":
|
|
799
|
+
m = re.search(r"language-([\w+-]+)", a.get("class") or "")
|
|
800
|
+
if m:
|
|
801
|
+
self.pre_lang = m.group(1)
|
|
802
|
+
return
|
|
803
|
+
if tag in ("h1", "h2", "h3", "h4", "h5", "h6") and self.cell is None:
|
|
804
|
+
self.flush()
|
|
805
|
+
self.heading = int(tag[1])
|
|
806
|
+
self.rep.keep("headings")
|
|
807
|
+
elif tag in self.BLOCK and self.cell is None:
|
|
808
|
+
self.flush()
|
|
809
|
+
if tag == "hr":
|
|
810
|
+
self.blank()
|
|
811
|
+
self.lines.append("---")
|
|
812
|
+
elif tag == "br":
|
|
813
|
+
self.text_target().append("<br>" if self.cell is not None else " \n")
|
|
814
|
+
elif tag in ("ul", "ol"):
|
|
815
|
+
self.flush()
|
|
816
|
+
self.lists.append([tag, 0])
|
|
817
|
+
elif tag == "li":
|
|
818
|
+
self.flush()
|
|
819
|
+
if self.lists:
|
|
820
|
+
self.lists[-1][1] += 1
|
|
821
|
+
kind, count = self.lists[-1]
|
|
822
|
+
indent = " " * (len(self.lists) - 1)
|
|
823
|
+
self.li_prefix = indent + ("%d. " % count if kind == "ol" else "- ")
|
|
824
|
+
self.rep.keep("list items")
|
|
825
|
+
elif tag == "blockquote":
|
|
826
|
+
self.flush()
|
|
827
|
+
self.quote += 1
|
|
828
|
+
elif tag == "pre":
|
|
829
|
+
self.flush()
|
|
830
|
+
self.pre, self.pre_buf, self.pre_lang = True, [], ""
|
|
831
|
+
elif tag in self.INLINE:
|
|
832
|
+
self.open_inline(tag)
|
|
833
|
+
elif tag == "a":
|
|
834
|
+
href = a.get("href") or ""
|
|
835
|
+
if href and not href.lower().startswith(("javascript:", "data:")):
|
|
836
|
+
self.text_target().append("[")
|
|
837
|
+
self.links.append(href)
|
|
838
|
+
else:
|
|
839
|
+
self.links.append(None)
|
|
840
|
+
elif tag == "img":
|
|
841
|
+
src = a.get("src") or ""
|
|
842
|
+
alt = a.get("alt") or ""
|
|
843
|
+
if src.startswith("data:") or not src:
|
|
844
|
+
self.rep.drop("images (embedded data)")
|
|
845
|
+
else:
|
|
846
|
+
self.text_target().append("" % (md_escape(alt), src))
|
|
847
|
+
self.rep.keep("image links")
|
|
848
|
+
elif tag == "table":
|
|
849
|
+
self.flush()
|
|
850
|
+
if self.table is not None:
|
|
851
|
+
self.rep.notes.append("a nested table was flattened into its parent cell")
|
|
852
|
+
else:
|
|
853
|
+
self.table = []
|
|
854
|
+
elif tag == "tr" and self.table is not None:
|
|
855
|
+
self.table.append([])
|
|
856
|
+
elif tag in ("td", "th") and self.table is not None:
|
|
857
|
+
if not self.table:
|
|
858
|
+
self.table.append([])
|
|
859
|
+
self.cell = []
|
|
860
|
+
span = int(a.get("colspan") or "1") if (a.get("colspan") or "1").isdigit() else 1
|
|
861
|
+
self._span = span
|
|
862
|
+
|
|
863
|
+
def handle_endtag(self, tag):
|
|
864
|
+
if tag in self.SKIP:
|
|
865
|
+
self.skip = max(0, self.skip - 1)
|
|
866
|
+
return
|
|
867
|
+
if self.skip:
|
|
868
|
+
return
|
|
869
|
+
if self.pre:
|
|
870
|
+
if tag == "pre":
|
|
871
|
+
code = "".join(self.pre_buf).strip("\n")
|
|
872
|
+
self.blank()
|
|
873
|
+
self.lines.append("```" + self.pre_lang)
|
|
874
|
+
self.lines.extend(code.split("\n"))
|
|
875
|
+
self.lines.append("```")
|
|
876
|
+
self.pre = False
|
|
877
|
+
self.rep.keep("code blocks")
|
|
878
|
+
return
|
|
879
|
+
if tag in ("h1", "h2", "h3", "h4", "h5", "h6") and self.cell is None:
|
|
880
|
+
self.flush()
|
|
881
|
+
self.heading = 0
|
|
882
|
+
elif tag in self.BLOCK and self.cell is None:
|
|
883
|
+
self.flush()
|
|
884
|
+
elif tag in ("ul", "ol"):
|
|
885
|
+
self.flush()
|
|
886
|
+
if self.lists:
|
|
887
|
+
self.lists.pop()
|
|
888
|
+
elif tag == "li":
|
|
889
|
+
self.flush()
|
|
890
|
+
elif tag == "blockquote":
|
|
891
|
+
self.flush()
|
|
892
|
+
self.quote = max(0, self.quote - 1)
|
|
893
|
+
elif tag in self.INLINE:
|
|
894
|
+
self.close_inline(tag)
|
|
895
|
+
elif tag == "a" and self.links:
|
|
896
|
+
href = self.links.pop()
|
|
897
|
+
if href:
|
|
898
|
+
self.text_target().append("](%s)" % href.replace(" ", "%20"))
|
|
899
|
+
elif tag in ("td", "th") and self.cell is not None and self.table is not None:
|
|
900
|
+
text = re.sub(r"\s+", " ", "".join(self.cell)).strip()
|
|
901
|
+
self.table[-1].append(text)
|
|
902
|
+
self.table[-1].extend([""] * (getattr(self, "_span", 1) - 1))
|
|
903
|
+
self.cell = None
|
|
904
|
+
elif tag == "table" and self.table is not None:
|
|
905
|
+
md = md_table(self.table)
|
|
906
|
+
self.table = None
|
|
907
|
+
if md:
|
|
908
|
+
self.blank()
|
|
909
|
+
self.lines.extend(md.split("\n"))
|
|
910
|
+
self.rep.keep("tables")
|
|
911
|
+
|
|
912
|
+
def handle_data(self, data):
|
|
913
|
+
if self.skip:
|
|
914
|
+
return
|
|
915
|
+
if self.pre:
|
|
916
|
+
self.pre_buf.append(data)
|
|
917
|
+
return
|
|
918
|
+
self.text_target().append(data if self.in_code else md_escape(data))
|
|
919
|
+
|
|
920
|
+
def open_inline(self, tag):
|
|
921
|
+
target = self.text_target()
|
|
922
|
+
self.inline_stack.append((tag, target, len(target)))
|
|
923
|
+
target.append(self.INLINE[tag])
|
|
924
|
+
if tag == "code":
|
|
925
|
+
self.in_code += 1
|
|
926
|
+
|
|
927
|
+
def close_inline(self, tag):
|
|
928
|
+
if tag == "code":
|
|
929
|
+
self.in_code = max(0, self.in_code - 1)
|
|
930
|
+
for k in range(len(self.inline_stack) - 1, -1, -1):
|
|
931
|
+
if self.inline_stack[k][0] == tag:
|
|
932
|
+
_, target, idx = self.inline_stack.pop(k)
|
|
933
|
+
break
|
|
934
|
+
else:
|
|
935
|
+
return
|
|
936
|
+
if idx >= len(target) or target is not self.text_target():
|
|
937
|
+
return
|
|
938
|
+
mark = self.INLINE[tag]
|
|
939
|
+
content = "".join(target[idx + 1:])
|
|
940
|
+
core = content.strip()
|
|
941
|
+
lead = content[:len(content) - len(content.lstrip())]
|
|
942
|
+
trail = content[len(content.rstrip()):]
|
|
943
|
+
target[idx:] = [lead + mark + core + mark + trail] if core else [content]
|
|
944
|
+
|
|
945
|
+
def result(self):
|
|
946
|
+
self.flush()
|
|
947
|
+
text = "\n".join(self.lines)
|
|
948
|
+
text = re.sub(r"\[\s*\]\([^)]*\)", "", text) # links with no text
|
|
949
|
+
return re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def html_to_md(path, rep):
|
|
953
|
+
raw = path.read_bytes()
|
|
954
|
+
m = re.search(br'charset=["\']?([\w-]+)', raw[:2048], re.I)
|
|
955
|
+
enc = m.group(1).decode("ascii", "ignore") if m else "utf-8"
|
|
956
|
+
try:
|
|
957
|
+
text = raw.decode(enc, errors="replace")
|
|
958
|
+
except LookupError:
|
|
959
|
+
text = raw.decode("utf-8", errors="replace")
|
|
960
|
+
skipped = len(re.findall(r"<(script|style)\b", text, re.I))
|
|
961
|
+
if skipped:
|
|
962
|
+
rep.drop("scripts/styles", skipped)
|
|
963
|
+
navs = len(re.findall(r"<nav\b", text, re.I))
|
|
964
|
+
if navs:
|
|
965
|
+
rep.drop("navigation menus", navs)
|
|
966
|
+
parser = HTMLToMD(rep)
|
|
967
|
+
parser.feed(text)
|
|
968
|
+
parser.close()
|
|
969
|
+
return parser.result()
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
# ---------------------------------------------------------------- csv / json / txt / pdf
|
|
973
|
+
|
|
974
|
+
def read_text(path, rep):
|
|
975
|
+
raw = path.read_bytes()
|
|
976
|
+
try:
|
|
977
|
+
return raw.decode("utf-8-sig")
|
|
978
|
+
except UnicodeDecodeError:
|
|
979
|
+
rep.notes.append("file is not UTF-8; read it as Latin-1, so check accented characters")
|
|
980
|
+
return raw.decode("latin-1")
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def csv_to_md(path, rep):
|
|
984
|
+
text = read_text(path, rep)
|
|
985
|
+
if path.suffix.lower() == ".tsv":
|
|
986
|
+
dialect = csv.excel_tab
|
|
987
|
+
else:
|
|
988
|
+
try:
|
|
989
|
+
dialect = csv.Sniffer().sniff(text[:4096], delimiters=",;\t|")
|
|
990
|
+
except csv.Error:
|
|
991
|
+
dialect = csv.excel
|
|
992
|
+
rows = list(csv.reader(io.StringIO(text), dialect))
|
|
993
|
+
if not rows:
|
|
994
|
+
sys.exit("error: %s has no rows" % path)
|
|
995
|
+
rep.keep("rows", len(rows) - 1)
|
|
996
|
+
return md_table([[md_escape(c) for c in r] for r in rows]) + "\n"
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def scalar(v):
|
|
1000
|
+
if isinstance(v, (dict, list)):
|
|
1001
|
+
return json.dumps(v, ensure_ascii=False, separators=(", ", ": "))
|
|
1002
|
+
if v is None:
|
|
1003
|
+
return ""
|
|
1004
|
+
if isinstance(v, bool):
|
|
1005
|
+
return "true" if v else "false"
|
|
1006
|
+
return str(v)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def objects_table(items):
|
|
1010
|
+
keys = []
|
|
1011
|
+
for it in items:
|
|
1012
|
+
for k in it:
|
|
1013
|
+
if k not in keys:
|
|
1014
|
+
keys.append(k)
|
|
1015
|
+
rows = [keys] + [[md_escape(scalar(it.get(k))) for k in keys] for it in items]
|
|
1016
|
+
return md_table(rows)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def json_to_md(path, rep):
|
|
1020
|
+
try:
|
|
1021
|
+
data = json.loads(read_text(path, rep))
|
|
1022
|
+
except ValueError as e:
|
|
1023
|
+
sys.exit("error: %s is not valid JSON: %s" % (path, e))
|
|
1024
|
+
if isinstance(data, list) and data and all(isinstance(x, dict) for x in data):
|
|
1025
|
+
rep.keep("rows", len(data))
|
|
1026
|
+
return objects_table(data) + "\n"
|
|
1027
|
+
if isinstance(data, list):
|
|
1028
|
+
return "\n".join("- " + md_escape(scalar(x)) for x in data) + "\n"
|
|
1029
|
+
if isinstance(data, dict):
|
|
1030
|
+
out, simple = [], []
|
|
1031
|
+
for k, v in data.items():
|
|
1032
|
+
if isinstance(v, list) and v and all(isinstance(x, dict) for x in v):
|
|
1033
|
+
out += ["## %s" % k, "", objects_table(v), ""]
|
|
1034
|
+
rep.keep("tables")
|
|
1035
|
+
else:
|
|
1036
|
+
simple.append([md_escape(str(k)), md_escape(scalar(v))])
|
|
1037
|
+
if simple:
|
|
1038
|
+
out = [md_table([["Key", "Value"]] + simple), ""] + out
|
|
1039
|
+
return "\n".join(out).rstrip() + "\n"
|
|
1040
|
+
return md_escape(scalar(data)) + "\n"
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
_GAP = re.compile(r"\s{3,}")
|
|
1044
|
+
_BULLET = re.compile(r"^[\u2022\u25e6\u25aa\u2023\u2043\u2013*-]\s+")
|
|
1045
|
+
_CODEISH_RE = re.compile(r"[{};]|^\s*(def|function|class|SELECT|FROM)\b")
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _codeish(line):
|
|
1049
|
+
"""Looks like code: diagram arrows, braces or semicolons, or a definition keyword.
|
|
1050
|
+
Arrows are a plain substring test (a regex for them reads like an HTML-comment filter)."""
|
|
1051
|
+
return "-->" in line or "==>" in line or bool(_CODEISH_RE.search(line))
|
|
1052
|
+
_CODE_START = re.compile(r"^\s*(def |class |function |import |from \S+ import |SELECT |WITH |#include|package |func |public |const |let |var )")
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _indent(line):
|
|
1056
|
+
return len(line) - len(line.lstrip(" "))
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _cells(line):
|
|
1060
|
+
return [c for c in _GAP.split(line.strip()) if c]
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _col_starts(line):
|
|
1064
|
+
"""Character positions where each of the line's text segments begins."""
|
|
1065
|
+
return set(m.start() for m in re.finditer(r"(?:(?<=^)|(?<= {3}))\S", line))
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _near(cols):
|
|
1069
|
+
"""Column positions, widened by two characters either side."""
|
|
1070
|
+
return set(c + d for c in cols for d in (-2, -1, 0, 1, 2))
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _multi_column(page):
|
|
1074
|
+
"""True when prose sits in side-by-side columns. Layout mode interleaves such columns line
|
|
1075
|
+
by line, so these pages are read in pdftotext's reading order instead. The signal: a text
|
|
1076
|
+
segment (after 3+ spaces, or a deep indent) starting at the same horizontal position on
|
|
1077
|
+
many lines, and carrying prose (3+ words on average). Table columns line up too, but
|
|
1078
|
+
their cells are short, so they don't qualify."""
|
|
1079
|
+
lines = [l for l in page.splitlines() if l.strip()]
|
|
1080
|
+
if len(lines) < 8:
|
|
1081
|
+
return False
|
|
1082
|
+
starts = {}
|
|
1083
|
+
for l in lines:
|
|
1084
|
+
for m in re.finditer(r"(?:^ {20,}|\S {3,})(\S.*?)(?= {3,}|$)", l):
|
|
1085
|
+
col = m.start(1) // 3 # bucket nearby positions together
|
|
1086
|
+
if col >= 7: # at least ~20 characters in from the left margin
|
|
1087
|
+
starts.setdefault(col, []).append(len(m.group(1).split()))
|
|
1088
|
+
for words in starts.values():
|
|
1089
|
+
if len(words) >= max(6, 0.25 * len(lines)) and sum(words) / len(words) >= 3:
|
|
1090
|
+
return True
|
|
1091
|
+
return False
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _pdf_page_plain(page):
|
|
1095
|
+
"""Reading-order text: paragraphs joined, nothing inferred."""
|
|
1096
|
+
paras = [re.sub(r"\s*\n\s*", " ", p).strip() for p in re.split(r"\n\s*\n", page)]
|
|
1097
|
+
out = []
|
|
1098
|
+
for p in paras:
|
|
1099
|
+
if p:
|
|
1100
|
+
out += [escape_line_start(md_escape(p)), ""]
|
|
1101
|
+
return out
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _pdf_page_md(page, rep):
|
|
1105
|
+
"""One page of `pdftotext -layout` text -> Markdown. Layout mode keeps line breaks,
|
|
1106
|
+
indentation and column alignment, which is what tables and lists are recovered from."""
|
|
1107
|
+
lines = page.splitlines()
|
|
1108
|
+
out, i, n = [], 0, len(lines)
|
|
1109
|
+
while i < n:
|
|
1110
|
+
if not lines[i].strip():
|
|
1111
|
+
i += 1
|
|
1112
|
+
continue
|
|
1113
|
+
# A table: two or more lines that each split into 2+ cells on wide gaps, with single
|
|
1114
|
+
# blank lines allowed between rows (PDF row spacing often reads as one). A line whose
|
|
1115
|
+
# text starts under one of the table's columns is a wrapped cell, so it belongs too.
|
|
1116
|
+
if len(_cells(lines[i])) >= 2:
|
|
1117
|
+
cols = _col_starts(lines[i])
|
|
1118
|
+
region, rows, j = [], 0, i
|
|
1119
|
+
while j < n:
|
|
1120
|
+
line = lines[j]
|
|
1121
|
+
if line.strip() and len(_cells(line)) >= 2:
|
|
1122
|
+
region.append(line); rows += 1; cols |= _col_starts(line); j += 1
|
|
1123
|
+
elif line.strip() and _col_starts(line) and _col_starts(line) <= _near(cols):
|
|
1124
|
+
region.append(line); j += 1
|
|
1125
|
+
elif not line.strip() and j + 1 < n and lines[j + 1].strip() and (
|
|
1126
|
+
len(_cells(lines[j + 1])) >= 2 or _col_starts(lines[j + 1]) <= _near(cols)):
|
|
1127
|
+
j += 1
|
|
1128
|
+
else:
|
|
1129
|
+
break
|
|
1130
|
+
if rows >= 2:
|
|
1131
|
+
widths = set(len(_cells(r)) for r in region)
|
|
1132
|
+
if len(widths) == 1 and rows == len(region):
|
|
1133
|
+
cells = [[cell_text(md_escape(c)) for c in _cells(r)] for r in region]
|
|
1134
|
+
width = len(cells[0])
|
|
1135
|
+
out.append("| " + " | ".join(cells[0]) + " |")
|
|
1136
|
+
out.append("|" + "---|" * width)
|
|
1137
|
+
out += ["| " + " | ".join(r) + " |" for r in cells[1:]]
|
|
1138
|
+
rep.keep("pdf tables", 1)
|
|
1139
|
+
else:
|
|
1140
|
+
out += ["```text"] + [r.rstrip() for r in region] + ["```"]
|
|
1141
|
+
rep.keep("pdf tables kept as aligned text", 1)
|
|
1142
|
+
out.append("")
|
|
1143
|
+
i = j
|
|
1144
|
+
continue
|
|
1145
|
+
# Otherwise a block of consecutive non-blank lines.
|
|
1146
|
+
block = []
|
|
1147
|
+
while i < n and lines[i].strip() and not (block and len(_cells(lines[i])) >= 2 and len(_cells(lines[i - 1])) >= 2):
|
|
1148
|
+
block.append(lines[i]); i += 1
|
|
1149
|
+
if not block: # a single table-like line on its own
|
|
1150
|
+
block = [lines[i]]; i += 1
|
|
1151
|
+
base = min(_indent(l) for l in block)
|
|
1152
|
+
if (base >= 8 and any(_codeish(l) for l in block)) or _CODE_START.match(block[0]):
|
|
1153
|
+
out += ["```"] + [l[base:].rstrip() for l in block] + ["```", ""]
|
|
1154
|
+
rep.keep("pdf code blocks", 1)
|
|
1155
|
+
continue
|
|
1156
|
+
first, rest = block[0], block[1:]
|
|
1157
|
+
indented = [l for l in rest if _indent(l) > _indent(first)]
|
|
1158
|
+
bulleted = [l for l in block if _BULLET.match(l.strip())]
|
|
1159
|
+
if bulleted or (rest and len(indented) == len(rest) and len(first.split()) <= 8):
|
|
1160
|
+
items = block
|
|
1161
|
+
if not bulleted: # a short lead line over an indented run: heading, then the list
|
|
1162
|
+
lead = first.strip()
|
|
1163
|
+
if lead and lead[-1] not in ".:;,!?":
|
|
1164
|
+
out.append("### " + md_escape(lead)); rep.keep("pdf headings guessed", 1)
|
|
1165
|
+
else:
|
|
1166
|
+
out.append(escape_line_start(md_escape(lead)))
|
|
1167
|
+
out.append("")
|
|
1168
|
+
items = rest
|
|
1169
|
+
# Indents within 2 spaces are one level: renderers offset numbered and bulleted
|
|
1170
|
+
# items by a space or so, which is not nesting.
|
|
1171
|
+
levels = []
|
|
1172
|
+
for ind in sorted(set(_indent(l) for l in items)):
|
|
1173
|
+
if not levels or ind - levels[-1] > 2:
|
|
1174
|
+
levels.append(ind)
|
|
1175
|
+
for l in items:
|
|
1176
|
+
depth = min(max(k for k, lv in enumerate(levels) if lv <= _indent(l) + 2), 3)
|
|
1177
|
+
text = _BULLET.sub("", l.strip())
|
|
1178
|
+
out.append(" " * depth + "- " + md_escape(text))
|
|
1179
|
+
out.append("")
|
|
1180
|
+
continue
|
|
1181
|
+
lead = block[0].strip()
|
|
1182
|
+
if len(block) > 1 and len(lead.split()) <= 10 and lead[-1:] not in ".:;,!?" and lead[:1].isupper() \
|
|
1183
|
+
and len(lead) < 0.7 * max(len(l.strip()) for l in block[1:]):
|
|
1184
|
+
out += ["### " + md_escape(lead), ""] # a title line over its first paragraph
|
|
1185
|
+
rep.keep("pdf headings guessed", 1)
|
|
1186
|
+
block = block[1:]
|
|
1187
|
+
para = " ".join(l.strip() for l in block)
|
|
1188
|
+
if len(block) == 1 and len(para.split()) <= 8 and para[-1:] not in ".:;,!?" and para[:1].isupper():
|
|
1189
|
+
out += ["### " + md_escape(para), ""]
|
|
1190
|
+
rep.keep("pdf headings guessed", 1)
|
|
1191
|
+
else:
|
|
1192
|
+
out += [escape_line_start(md_escape(para)), ""]
|
|
1193
|
+
return out
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def pdf_to_md(path, rep):
|
|
1197
|
+
exe = shutil.which("pdftotext")
|
|
1198
|
+
if not exe:
|
|
1199
|
+
sys.exit("error: reading PDF needs `pdftotext` (part of Poppler), which is not on PATH.\n"
|
|
1200
|
+
" macOS: brew install poppler\n"
|
|
1201
|
+
" Debian/Ubuntu: sudo apt install poppler-utils\n"
|
|
1202
|
+
" Windows: install Poppler for Windows and add its bin folder to PATH\n"
|
|
1203
|
+
"Then rerun. Or export the PDF to .docx or .txt and convert that.")
|
|
1204
|
+
def run(mode):
|
|
1205
|
+
proc = subprocess.run([exe] + mode + ["-enc", "UTF-8", str(path), "-"], stdout=subprocess.PIPE,
|
|
1206
|
+
stderr=subprocess.PIPE, check=False)
|
|
1207
|
+
if proc.returncode != 0:
|
|
1208
|
+
sys.exit("error: pdftotext failed: %s" % proc.stderr.decode("utf-8", "replace").strip())
|
|
1209
|
+
text = proc.stdout.decode("utf-8", "replace")
|
|
1210
|
+
pages = text.split("\f")
|
|
1211
|
+
return pages[:-1] if pages and not pages[-1].strip() else pages
|
|
1212
|
+
|
|
1213
|
+
layout, reading = run(["-layout"]), run([])
|
|
1214
|
+
if not "".join(layout).strip():
|
|
1215
|
+
sys.exit("error: %s has no text layer (probably scanned). It needs OCR first; "
|
|
1216
|
+
"this script does not do OCR." % path)
|
|
1217
|
+
out, plain_pages = [], []
|
|
1218
|
+
for n, page in enumerate(layout, 1):
|
|
1219
|
+
out += ["<!-- page %d -->" % n, ""]
|
|
1220
|
+
if _multi_column(page) and n <= len(reading):
|
|
1221
|
+
out += _pdf_page_plain(reading[n - 1])
|
|
1222
|
+
plain_pages.append(n)
|
|
1223
|
+
else:
|
|
1224
|
+
out += _pdf_page_md(page, rep)
|
|
1225
|
+
pages = layout
|
|
1226
|
+
if plain_pages:
|
|
1227
|
+
rep.notes.append("page(s) %s have side-by-side columns, so they were read in column order with "
|
|
1228
|
+
"no tables, lists or headings inferred" % ", ".join(str(p) for p in plain_pages))
|
|
1229
|
+
rep.keep("pages", len(pages))
|
|
1230
|
+
rep.notes.append("PDF gives no structure, so it is inferred from the layout: tables from aligned columns, "
|
|
1231
|
+
"lists from indentation, and short standalone lines as ### headings. Check them against the PDF; "
|
|
1232
|
+
"images and charts are not recovered")
|
|
1233
|
+
return "\n".join(out).rstrip() + "\n"
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
# ---------------------------------------------------------------- main
|
|
1237
|
+
|
|
1238
|
+
HANDLERS = {
|
|
1239
|
+
".docx": docx_to_md, ".pptx": pptx_to_md, ".xlsx": xlsx_to_md, ".xlsm": xlsx_to_md,
|
|
1240
|
+
".html": html_to_md, ".htm": html_to_md, ".csv": csv_to_md, ".tsv": csv_to_md,
|
|
1241
|
+
".json": json_to_md, ".pdf": pdf_to_md,
|
|
1242
|
+
}
|
|
1243
|
+
LEGACY = {".doc": ".docx", ".ppt": ".pptx", ".xls": ".xlsx", ".rtf": ".docx", ".odt": ".docx",
|
|
1244
|
+
".pages": ".docx", ".key": ".pptx", ".numbers": ".xlsx"}
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def main(argv=None):
|
|
1248
|
+
ap = argparse.ArgumentParser(description="Convert a document to Markdown for another skill to read.")
|
|
1249
|
+
ap.add_argument("input", help=".docx .pptx .xlsx .html .csv .tsv .json .txt .md .pdf")
|
|
1250
|
+
ap.add_argument("-o", "--output", required=True, help="Markdown file to write, or - for stdout")
|
|
1251
|
+
ap.add_argument("--max-rows", type=int, default=0,
|
|
1252
|
+
help="xlsx only: keep at most this many data rows per sheet (0 = all)")
|
|
1253
|
+
a = ap.parse_args(argv)
|
|
1254
|
+
|
|
1255
|
+
path = Path(a.input)
|
|
1256
|
+
if not path.is_file():
|
|
1257
|
+
sys.exit("error: input not found: %s" % a.input)
|
|
1258
|
+
ext = path.suffix.lower()
|
|
1259
|
+
rep = Report()
|
|
1260
|
+
if ext in LEGACY:
|
|
1261
|
+
sys.exit("error: %s files are not supported. Save or export it as %s and rerun%s."
|
|
1262
|
+
% (ext, LEGACY[ext], " (on macOS: textutil -convert docx <file>)"
|
|
1263
|
+
if ext in (".doc", ".rtf", ".odt") else ""))
|
|
1264
|
+
if ext in (".txt", ".md", ".markdown", ".text", ""):
|
|
1265
|
+
md = read_text(path, rep)
|
|
1266
|
+
if not md.endswith("\n"):
|
|
1267
|
+
md += "\n"
|
|
1268
|
+
elif ext in HANDLERS:
|
|
1269
|
+
fn = HANDLERS[ext]
|
|
1270
|
+
md = fn(path, rep, a.max_rows) if fn is xlsx_to_md else fn(path, rep)
|
|
1271
|
+
else:
|
|
1272
|
+
sys.exit("error: unsupported file type %r. Supported: %s, .txt, .md"
|
|
1273
|
+
% (ext, ", ".join(sorted(HANDLERS))))
|
|
1274
|
+
|
|
1275
|
+
if not md.strip():
|
|
1276
|
+
rep.notes.append("the conversion produced no text")
|
|
1277
|
+
if a.output == "-":
|
|
1278
|
+
sys.stdout.write(md)
|
|
1279
|
+
else:
|
|
1280
|
+
out = Path(a.output)
|
|
1281
|
+
if out.parent and not out.parent.exists():
|
|
1282
|
+
sys.exit("error: output folder does not exist: %s" % out.parent)
|
|
1283
|
+
out.write_text(md, encoding="utf-8")
|
|
1284
|
+
|
|
1285
|
+
kept = ", ".join("%d %s" % (v, k) for k, v in rep.kept.items()) or "text only"
|
|
1286
|
+
dest = "stdout" if a.output == "-" else a.output
|
|
1287
|
+
print("converted %s -> %s: %s" % (path.name, dest, kept), file=sys.stderr)
|
|
1288
|
+
if rep.dropped:
|
|
1289
|
+
print("dropped (not in the Markdown): " +
|
|
1290
|
+
", ".join("%d %s" % (v, k) for k, v in rep.dropped.items()), file=sys.stderr)
|
|
1291
|
+
else:
|
|
1292
|
+
print("dropped: nothing detected", file=sys.stderr)
|
|
1293
|
+
for n in rep.notes:
|
|
1294
|
+
print("note: %s" % n, file=sys.stderr)
|
|
1295
|
+
return 0
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
if __name__ == "__main__":
|
|
1299
|
+
sys.exit(main())
|