skilldrop-cli 0.16.35 → 0.16.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agents/README.md +4 -1
- package/agents/docs-reviewer.md +47 -0
- package/agents/finding-adjudicator.md +49 -0
- package/agents/test-reviewer.md +52 -0
- package/catalogue.json +19 -7
- package/model-routing.json +48 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/agent-config-review/SKILL.md +116 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/eval_queries.json +12 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/evals.json +75 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/db-migrator.md +8 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/reviewer.md +9 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.json +48 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.local.json +5 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.mcp.json +22 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/package.json +12 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/2/.cursor/mcp.json +17 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/2/config.toml +21 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/my-user-settings.json +11 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/agents/release-notes.md +13 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/settings.json +39 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.mcp.json +18 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/package.json +14 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/.github/workflows/codex-triage.yml +29 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/AGENTS.md +16 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/codex/config.toml +34 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.claude/settings.json +22 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.mcp.json +9 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +81 -0
- package/packs/ai-engineering/skills/agent-config-review/manifest.json +60 -0
- package/packs/ai-engineering/skills/agent-config-review/reference.md +205 -0
- package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +989 -0
- package/packs/ai-engineering/skills/agent-incident-response/SKILL.md +175 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/eval_queries.json +14 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/evals.json +107 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/agent-session-excerpt.txt +11 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/ci-build-log.txt +24 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/incident-notes.md +19 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/mcp-config.json +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/session-transcript.txt +8 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/agent-session.log +70 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/infra/staging-db.tf +25 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/job.yaml +19 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/run.log +13 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/usage-export-2026-10-20.csv +13 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/.mcp.json +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/git-log.txt +12 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync-excerpt.txt +30 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync.log +15 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/ssh-config.txt +16 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/.github/workflows/ci.yml +28 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/SECURITY.md +8 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/issue-479.md +35 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/package.json +15 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/pr-482.diff +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/run-log.txt +30 -0
- package/packs/ai-engineering/skills/agent-incident-response/examples/injected-issue-comment.md +57 -0
- package/packs/ai-engineering/skills/agent-incident-response/manifest.json +55 -0
- package/packs/ai-engineering/skills/agent-incident-response/reference.md +152 -0
- package/packs/ai-engineering/skills/agent-incident-response/scripts/find_secret_exposure.py +239 -0
- package/packs/ai-engineering/skills/agent-incident-response/scripts/usage_split.py +194 -0
- package/packs/ai-engineering/skills/judge-audit/SKILL.md +157 -0
- package/packs/ai-engineering/skills/judge-audit/evals/eval_queries.json +11 -0
- package/packs/ai-engineering/skills/judge-audit/evals/evals.json +136 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/1/judgements.csv +321 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/1/notes.md +28 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/2/pairwise.csv +101 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/2/run-notes.md +16 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/4/grades.csv +97 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/5/pairwise_judgements.jsonl +120 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/6/rag-judge/README.md +36 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/6/rag-judge/judgments.csv +301 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/7/moderation/gold_labels.csv +101 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/7/moderation/run_1.jsonl +100 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/7/moderation/run_2.jsonl +100 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/8/patches.csv +241 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/9/adequacy_ratings.csv +151 -0
- package/packs/ai-engineering/skills/judge-audit/examples/summary-panel.md +70 -0
- package/packs/ai-engineering/skills/judge-audit/manifest.json +44 -0
- package/packs/ai-engineering/skills/judge-audit/reference.md +176 -0
- package/packs/ai-engineering/skills/judge-audit/scripts/judge_stats.py +1735 -0
- package/packs/converters/skills/file-to-markdown/SKILL.md +2 -1
- package/packs/converters/skills/file-to-markdown/evals/eval_queries.json +4 -0
- package/packs/converters/skills/file-to-markdown/manifest.json +2 -2
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +253 -3
- package/packs/converters/skills/md-to-confluence/SKILL.md +139 -0
- package/packs/converters/skills/md-to-confluence/evals/eval_queries.json +42 -0
- package/packs/converters/skills/md-to-confluence/evals/evals.json +89 -0
- package/packs/converters/skills/md-to-confluence/evals/files/1/img/latency-dashboard.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/1/payments-runbook.md +60 -0
- package/packs/converters/skills/md-to-confluence/evals/files/2/deploy-checklist.md +33 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/img/after/screen.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/img/before/screen.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/release-notes-4.2.md +49 -0
- package/packs/converters/skills/md-to-confluence/evals/files/4/docs/img/topology.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/4/docs/ingest-pipeline-design.md +65 -0
- package/packs/converters/skills/md-to-confluence/evals/files/5/support/troubleshooting-sso.md +57 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/img/queue-depth.png +0 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.fixed.md +21 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.md +21 -0
- package/packs/converters/skills/md-to-confluence/examples/queue-runbook.md +57 -0
- package/packs/converters/skills/md-to-confluence/manifest.json +52 -0
- package/packs/converters/skills/md-to-confluence/reference.md +92 -0
- package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +821 -0
- package/packs/converters/skills/md-to-pptx/SKILL.md +131 -0
- package/packs/converters/skills/md-to-pptx/evals/eval_queries.json +12 -0
- package/packs/converters/skills/md-to-pptx/evals/evals.json +84 -0
- package/packs/converters/skills/md-to-pptx/evals/files/1/img/defect-burndown.png +0 -0
- package/packs/converters/skills/md-to-pptx/evals/files/1/status-report.md +75 -0
- package/packs/converters/skills/md-to-pptx/evals/files/3/brand.json +12 -0
- package/packs/converters/skills/md-to-pptx/evals/files/3/night-shift-induction.md +53 -0
- package/packs/converters/skills/md-to-pptx/evals/files/4/charts/ontime.png +0 -0
- package/packs/converters/skills/md-to-pptx/evals/files/4/qbr-q3.md +85 -0
- package/packs/converters/skills/md-to-pptx/evals/files/5/vendor-sync-notes.md +28 -0
- package/packs/converters/skills/md-to-pptx/examples/support-weekly-input.md +48 -0
- package/packs/converters/skills/md-to-pptx/examples/support-weekly.md +75 -0
- package/packs/converters/skills/md-to-pptx/examples/tickets-by-week.png +0 -0
- package/packs/converters/skills/md-to-pptx/manifest.json +49 -0
- package/packs/converters/skills/md-to-pptx/reference.md +139 -0
- package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +1841 -0
- package/packs/dev-team/loops/modernize/LOOP.md +124 -0
- package/packs/dev-team/loops/modernize/evals/eval_queries.json +9 -0
- package/packs/dev-team/loops/modernize/evals/evals.json +27 -0
- package/packs/dev-team/loops/modernize/loop.json +60 -0
- package/packs/dev-team/skills/behaviour-equivalence/SKILL.md +155 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/eval_queries.json +11 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/evals.json +76 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/cases.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/new.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/notes.md +15 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old_rerun.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v1.py +45 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v2.py +65 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/rules.json +10 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/3/diff-summary.txt +16 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/diff.txt +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py2.py +16 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py3.py +24 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/input.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/new_output.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/old_output.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/TEAM_NOTES.md +19 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/normalisation.yaml +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/new_run.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run1.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run2.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/examples/delivery-date.md +78 -0
- package/packs/dev-team/skills/behaviour-equivalence/manifest.json +33 -0
- package/packs/dev-team/skills/behaviour-equivalence/reference.md +97 -0
- package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +524 -0
- package/packs/dev-team/skills/behaviour-equivalence/templates/accepted.json +10 -0
- package/packs/dev-team/skills/behaviour-equivalence/templates/rules.json +7 -0
- package/packs/frontend/pack.json +61 -0
- package/packs/frontend/skills/bundle-size-review/SKILL.md +114 -0
- package/packs/frontend/skills/bundle-size-review/evals/eval_queries.json +12 -0
- package/packs/frontend/skills/bundle-size-review/evals/evals.json +108 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/1/package.json +28 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/1/src/utils/format.js +26 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/1/stats.json +1334 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/2/PR.md +21 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/2/meta-after.json +440 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/2/meta-before.json +360 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/3/package.json +29 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/3/stats.json +1583 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/4/src/components/CommentBox.jsx +24 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/4/stats/after.json +299 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/4/stats/before.json +275 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/.browserslistrc +3 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/build/meta.json +371 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/build.mjs +18 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/package-lock.json +39 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/package.json +22 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/src/components/Icon.jsx +8 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/src/polyfills.js +7 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/5/src/utils/groupRows.js +4 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/.browserslistrc +3 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/package.json +25 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/App.tsx +8 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/chartLocales.ts +16 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/index.ts +8 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/messages/de.ts +5 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/messages/en.ts +5 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/useLocale.ts +14 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/main.tsx +10 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/pages/Home.tsx +14 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/pages/Reports.tsx +31 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/src/routes.tsx +17 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/stats.json +1869 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/6/vite.config.ts +13 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/package.json +26 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/src/App.tsx +27 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/src/pages/Analytics.tsx +10 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/src/pages/Users.tsx +13 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/stats.json +1421 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/webpack.config.js +25 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/package.json +27 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/App.tsx +18 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/index.tsx +4 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/pages/Home.tsx +10 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/pages/Product.tsx +13 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/stats.json +1496 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/webpack.config.js +26 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/package.json +9 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/Button.css +3 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/Button.js +14 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/Chart.js +21 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/index.js +22 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/package.json +20 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/Button.css +3 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/Button.tsx +9 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/Chart.tsx +14 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/index.ts +6 -0
- package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/tsconfig.json +14 -0
- package/packs/frontend/skills/bundle-size-review/examples/inputs/stats-after.json +1358 -0
- package/packs/frontend/skills/bundle-size-review/examples/inputs/stats-before.json +1334 -0
- package/packs/frontend/skills/bundle-size-review/examples/larkspur-sparkline.md +52 -0
- package/packs/frontend/skills/bundle-size-review/manifest.json +52 -0
- package/packs/frontend/skills/bundle-size-review/reference.md +147 -0
- package/packs/frontend/skills/bundle-size-review/scripts/bundle_stats.py +1633 -0
- package/packs/frontend/skills/rendering-strategy/SKILL.md +149 -0
- package/packs/frontend/skills/rendering-strategy/evals/eval_queries.json +38 -0
- package/packs/frontend/skills/rendering-strategy/evals/evals.json +77 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/1/fernhill-app.md +40 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/3/routes.md +33 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/build-notes.txt +5 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/nuxt.config.ts +19 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/pages/recipes/[slug].vue +37 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/routes.md +15 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/routes.txt +18 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/hooks.server.ts +14 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/(marketing)/pricing/+page.server.ts +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/+layout.server.ts +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/svelte.config.js +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/traffic.md +18 -0
- package/packs/frontend/skills/rendering-strategy/examples/saas-site.md +42 -0
- package/packs/frontend/skills/rendering-strategy/manifest.json +50 -0
- package/packs/frontend/skills/rendering-strategy/reference.md +116 -0
- package/packs/frontend/skills/rendering-strategy/scripts/render_load.py +181 -0
- package/packs/frontend/skills/web-performance-review/SKILL.md +106 -0
- package/packs/frontend/skills/web-performance-review/evals/eval_queries.json +38 -0
- package/packs/frontend/skills/web-performance-review/evals/evals.json +76 -0
- package/packs/frontend/skills/web-performance-review/evals/files/1/psi.json +1065 -0
- package/packs/frontend/skills/web-performance-review/evals/files/2/after.json +359 -0
- package/packs/frontend/skills/web-performance-review/evals/files/2/before.json +375 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/psi-pdp-mobile-2026-10-19.json +171 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-attribution-oct14-19.csv +6 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-daily.csv +15 -0
- package/packs/frontend/skills/web-performance-review/evals/files/5/crux-home-phone.json +35 -0
- package/packs/frontend/skills/web-performance-review/evals/files/5/lighthouse-home-mobile.json +173 -0
- package/packs/frontend/skills/web-performance-review/examples/quayside-article-lhr.json +480 -0
- package/packs/frontend/skills/web-performance-review/examples/quayside-article.md +103 -0
- package/packs/frontend/skills/web-performance-review/manifest.json +49 -0
- package/packs/frontend/skills/web-performance-review/reference.md +105 -0
- package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +743 -0
- package/packs/product-manager/skills/lean-canvas/SKILL.md +162 -0
- package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +42 -0
- package/packs/product-manager/skills/lean-canvas/evals/evals.json +82 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +54 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +30 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +44 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +5 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +40 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +18 -0
- package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +72 -0
- package/packs/product-manager/skills/lean-canvas/manifest.json +34 -0
- package/packs/product-manager/skills/lean-canvas/reference.md +102 -0
- package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +211 -0
- package/packs/solution-architect/skills/api-contract-draft/SKILL.md +2 -2
- package/packs/solution-architect/skills/api-contract-draft/manifest.json +28 -6
- package/packs/solution-architect/skills/event-contract-draft/SKILL.md +167 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/eval_queries.json +38 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/evals.json +76 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/1/order-events-brief.md +45 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/4/NOTES.md +23 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/4/schemas/shipment-update.json +40 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/5/invoice.example.json +25 -0
- package/packs/solution-architect/skills/event-contract-draft/examples/order-service.asyncapi.json +246 -0
- package/packs/solution-architect/skills/event-contract-draft/examples/order-service.md +92 -0
- package/packs/solution-architect/skills/event-contract-draft/manifest.json +25 -0
- package/packs/solution-architect/skills/event-contract-draft/reference.md +113 -0
- package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +502 -0
- package/packs/solution-architect/skills/event-contract-draft/templates/asyncapi-skeleton.json +144 -0
- package/packs/sre-oncall/skills/slo-spec/SKILL.md +161 -0
- package/packs/sre-oncall/skills/slo-spec/evals/eval_queries.json +38 -0
- package/packs/sre-oncall/skills/slo-spec/evals/evals.json +75 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/1/order-service.md +51 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/3/search-slo-status.md +30 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/alerts/payments-api.rules.yaml +16 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/docs/dependencies.md +8 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/metrics/summary-90d.md +51 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/5/ops-notes.md +34 -0
- package/packs/sre-oncall/skills/slo-spec/examples/ticket-booking.md +88 -0
- package/packs/sre-oncall/skills/slo-spec/manifest.json +34 -0
- package/packs/sre-oncall/skills/slo-spec/reference.md +113 -0
- package/packs/sre-oncall/skills/slo-spec/scripts/slo_math.py +449 -0
- package/packs/sre-oncall/skills/slo-spec/templates/slo-spec.md +35 -0
|
@@ -0,0 +1,1841 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert a Markdown document to a PowerPoint deck (.pptx). Stdlib only: zip + XML by hand.
|
|
3
|
+
|
|
4
|
+
Mapping (one rule each, so the deck is predictable):
|
|
5
|
+
title slide --title, else the first # heading, else front matter title:; subtitle from front
|
|
6
|
+
matter subtitle/author/presenter/date, or one short paragraph right under the #
|
|
7
|
+
## heading starts a slide; its text is the slide title. A later # heading starts a section
|
|
8
|
+
slide (or a content slide, when text follows it before the next ##)
|
|
9
|
+
### to ###### a bold subheading line on the current slide
|
|
10
|
+
lists bulleted / numbered paragraphs, nesting kept to three levels (deeper is flattened)
|
|
11
|
+
paragraphs body text; > quotes in italic
|
|
12
|
+
pipe tables real PowerPoint tables (a:tbl) with a shaded header row; long tables split by
|
|
13
|
+
rows onto continued slides, the header row repeated
|
|
14
|
+
images local PNG / JPEG embedded, scaled down to fit (never up)
|
|
15
|
+
code blocks a shaded monospace text box
|
|
16
|
+
--- forces a new slide, titled "<title> (continued)"
|
|
17
|
+
<!-- notes: ... --> speaker notes for the slide it sits on (the only notes convention)
|
|
18
|
+
|
|
19
|
+
Overflow: text height is estimated per slide; a slide that would overflow is split into
|
|
20
|
+
"<title> (continued)" slides instead of shrinking text. Every split is reported.
|
|
21
|
+
|
|
22
|
+
Dropped or changed, each with a WARN line naming the source line: raw HTML blocks and other
|
|
23
|
+
HTML comments, inline HTML tags (text kept), footnotes, remote / missing / non-PNG-JPEG images,
|
|
24
|
+
nesting past three levels, code or tables inside list items, front matter (title fields used),
|
|
25
|
+
links to local files, a "Notes:" paragraph (kept on the slide, not made into notes).
|
|
26
|
+
|
|
27
|
+
After writing, the file is re-opened and checked: every XML part parses, every internal
|
|
28
|
+
relationship target exists, and every part has a content type.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
python3 md_to_pptx.py input.md -o deck.pptx [--brand brand.json] [--title "..."]
|
|
32
|
+
[--aspect 16:9|4:3]
|
|
33
|
+
(input "-" reads Markdown from stdin; image paths are then relative to the current folder)
|
|
34
|
+
"""
|
|
35
|
+
import argparse
|
|
36
|
+
import datetime
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import posixpath
|
|
40
|
+
import re
|
|
41
|
+
import struct
|
|
42
|
+
import sys
|
|
43
|
+
import zipfile
|
|
44
|
+
from urllib.parse import quote
|
|
45
|
+
from xml.dom import minidom
|
|
46
|
+
from xml.sax.saxutils import escape as _esc
|
|
47
|
+
|
|
48
|
+
WARNINGS = []
|
|
49
|
+
QUIET = [False] # set while measuring, so a cell parsed twice warns once
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def warn(line, msg):
|
|
53
|
+
if QUIET[0]:
|
|
54
|
+
return
|
|
55
|
+
where = "line %d: " % line if line else ""
|
|
56
|
+
if where + msg in WARNINGS:
|
|
57
|
+
return
|
|
58
|
+
WARNINGS.append(where + msg)
|
|
59
|
+
print("WARN %s%s" % (where, msg), file=sys.stderr)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def die(msg, code=1):
|
|
63
|
+
print("ERROR %s" % msg, file=sys.stderr)
|
|
64
|
+
sys.exit(code)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_BAD_XML = dict.fromkeys(c for c in range(0x20) if c not in (0x09, 0x0A, 0x0D))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def x(s):
|
|
71
|
+
"""Escape text for XML element content and attributes."""
|
|
72
|
+
return _esc(s.translate(_BAD_XML), {'"': """})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# =========================================================================== block parsing
|
|
76
|
+
|
|
77
|
+
RE_ATX = re.compile(r"^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$")
|
|
78
|
+
RE_SETEXT = re.compile(r"^ {0,3}(=+|-+)[ \t]*$")
|
|
79
|
+
RE_HR = re.compile(r"^ {0,3}((\*[ \t]*){3,}|(-[ \t]*){3,}|(_[ \t]*){3,})$")
|
|
80
|
+
RE_FENCE = re.compile(r"^( {0,3})(`{3,}|~{3,})[ \t]*([^`]*)$")
|
|
81
|
+
RE_ITEM = re.compile(r"^([ \t]*)([-*+]|\d{1,9}[.)])(?:[ \t]+(.*)|$)")
|
|
82
|
+
RE_TABLE_SEP = re.compile(r"^[ \t]*\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$")
|
|
83
|
+
RE_HTML_BLOCK = re.compile(r"^ {0,3}<(/?[A-Za-z][A-Za-z0-9-]*(\s[^>]*)?/?>|/?[A-Za-z][A-Za-z0-9-]*\s*$)")
|
|
84
|
+
RE_FOOTDEF = re.compile(r"^ {0,3}\[\^[^\]]+\]:")
|
|
85
|
+
RE_REFDEF = re.compile(r"^ {0,3}\[([^\]^][^\]]*)\]:[ \t]*<?(\S+?)>?(?:[ \t]+[\"'(].*[\"')])?[ \t]*$")
|
|
86
|
+
RE_NOTES = re.compile(r"^\s*(?:speaker[ _-]?)?notes?\s*:\s*(.*)$", re.I | re.S)
|
|
87
|
+
RE_NOTES_PARA = re.compile(r"^\s*(?:\*\*|__)?\s*(?:speaker\s+)?notes\s*(?:\*\*|__)?\s*:", re.I)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def expand_tabs(line):
|
|
91
|
+
return line.expandtabs(4)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def indent_of(line):
|
|
95
|
+
return len(line) - len(line.lstrip(" "))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def split_row(line):
|
|
99
|
+
s = line.strip()
|
|
100
|
+
if s.startswith("|"):
|
|
101
|
+
s = s[1:]
|
|
102
|
+
if s.endswith("|") and not s.endswith("\\|"):
|
|
103
|
+
s = s[:-1]
|
|
104
|
+
cells, cur, i = [], "", 0
|
|
105
|
+
in_code = False
|
|
106
|
+
while i < len(s):
|
|
107
|
+
c = s[i]
|
|
108
|
+
if c == "\\" and i + 1 < len(s) and s[i + 1] == "|":
|
|
109
|
+
cur += "|"
|
|
110
|
+
i += 2
|
|
111
|
+
continue
|
|
112
|
+
if c == "`":
|
|
113
|
+
in_code = not in_code
|
|
114
|
+
if c == "|" and not in_code:
|
|
115
|
+
cells.append(cur.strip())
|
|
116
|
+
cur = ""
|
|
117
|
+
else:
|
|
118
|
+
cur += c
|
|
119
|
+
i += 1
|
|
120
|
+
cells.append(cur.strip())
|
|
121
|
+
return cells
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_blocks(lines, base_line, refs):
|
|
125
|
+
"""lines: list of str (no newline). Returns a list of block dicts, each with 'line'."""
|
|
126
|
+
blocks = []
|
|
127
|
+
i = 0
|
|
128
|
+
n = len(lines)
|
|
129
|
+
para = []
|
|
130
|
+
|
|
131
|
+
def flush_para():
|
|
132
|
+
if para:
|
|
133
|
+
blocks.append({"type": "para", "lines": list(para), "line": para[0][0]})
|
|
134
|
+
del para[:]
|
|
135
|
+
|
|
136
|
+
while i < n:
|
|
137
|
+
line = expand_tabs(lines[i])
|
|
138
|
+
ln = base_line + i
|
|
139
|
+
stripped = line.strip()
|
|
140
|
+
|
|
141
|
+
if not stripped:
|
|
142
|
+
flush_para()
|
|
143
|
+
i += 1
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
m = RE_FENCE.match(line)
|
|
147
|
+
if m:
|
|
148
|
+
flush_para()
|
|
149
|
+
fence, info = m.group(2), m.group(3).strip()
|
|
150
|
+
ind = len(m.group(1))
|
|
151
|
+
body = []
|
|
152
|
+
i += 1
|
|
153
|
+
closed = False
|
|
154
|
+
while i < n:
|
|
155
|
+
l2 = expand_tabs(lines[i])
|
|
156
|
+
if l2.strip().startswith(fence[0] * len(fence)) and set(l2.strip()) <= {fence[0]}:
|
|
157
|
+
closed = True
|
|
158
|
+
i += 1
|
|
159
|
+
break
|
|
160
|
+
body.append(l2[ind:] if indent_of(l2) >= ind else l2.lstrip(" "))
|
|
161
|
+
i += 1
|
|
162
|
+
if not closed:
|
|
163
|
+
warn(ln, "code fence never closed; ran to end of file")
|
|
164
|
+
lang = info.split()[0] if info else ""
|
|
165
|
+
if lang.lower() == "mermaid":
|
|
166
|
+
warn(ln, "mermaid block kept as code text, not rendered as a diagram")
|
|
167
|
+
blocks.append({"type": "code", "text": "\n".join(body), "line": ln})
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if not para and indent_of(line) >= 4:
|
|
171
|
+
body = []
|
|
172
|
+
while i < n and (not lines[i].strip() or indent_of(expand_tabs(lines[i])) >= 4):
|
|
173
|
+
body.append(expand_tabs(lines[i])[4:])
|
|
174
|
+
i += 1
|
|
175
|
+
while body and not body[-1].strip():
|
|
176
|
+
body.pop()
|
|
177
|
+
blocks.append({"type": "code", "text": "\n".join(body), "line": ln})
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if stripped.startswith("<!--"):
|
|
181
|
+
flush_para()
|
|
182
|
+
buf = [stripped]
|
|
183
|
+
while "-->" not in buf[-1] and i + 1 < n:
|
|
184
|
+
i += 1
|
|
185
|
+
buf.append(lines[i])
|
|
186
|
+
i += 1
|
|
187
|
+
body = "\n".join(buf)
|
|
188
|
+
if "-->" not in body:
|
|
189
|
+
warn(ln, "HTML comment never closed; ran to end of file")
|
|
190
|
+
tail = body.split("-->", 1)[1].strip() if "-->" in body else ""
|
|
191
|
+
inner = body[4:].split("-->", 1)[0]
|
|
192
|
+
nm = RE_NOTES.match(inner)
|
|
193
|
+
if nm:
|
|
194
|
+
text = "\n".join(l.strip() for l in nm.group(1).strip().split("\n"))
|
|
195
|
+
blocks.append({"type": "notes", "text": text, "line": ln})
|
|
196
|
+
else:
|
|
197
|
+
warn(ln, "HTML comment dropped (only <!-- notes: ... --> becomes speaker notes)")
|
|
198
|
+
if tail:
|
|
199
|
+
warn(ln, "text after the closing --> on the same line dropped")
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
m = RE_ATX.match(line)
|
|
203
|
+
if m:
|
|
204
|
+
flush_para()
|
|
205
|
+
level = len(m.group(1))
|
|
206
|
+
text = (m.group(2) or "").strip()
|
|
207
|
+
blocks.append({"type": "heading", "level": level, "text": text, "line": ln})
|
|
208
|
+
i += 1
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if para and RE_SETEXT.match(line) and len(para) == 1:
|
|
212
|
+
level = 1 if stripped.startswith("=") else 2
|
|
213
|
+
blocks.append({"type": "heading", "level": level, "text": para[0][1].strip(), "line": para[0][0]})
|
|
214
|
+
del para[:]
|
|
215
|
+
i += 1
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
if RE_HR.match(line):
|
|
219
|
+
flush_para()
|
|
220
|
+
blocks.append({"type": "hr", "line": ln})
|
|
221
|
+
i += 1
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
if RE_FOOTDEF.match(line):
|
|
225
|
+
flush_para()
|
|
226
|
+
warn(ln, "footnote definition dropped (footnotes are not supported)")
|
|
227
|
+
i += 1
|
|
228
|
+
while i < n and lines[i].strip() and indent_of(expand_tabs(lines[i])) >= 2:
|
|
229
|
+
i += 1
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
m = RE_REFDEF.match(line)
|
|
233
|
+
if m and not para:
|
|
234
|
+
refs[m.group(1).strip().lower()] = m.group(2)
|
|
235
|
+
i += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if not para and RE_HTML_BLOCK.match(line):
|
|
239
|
+
start = ln
|
|
240
|
+
while i < n and lines[i].strip():
|
|
241
|
+
i += 1
|
|
242
|
+
end = base_line + i - 1
|
|
243
|
+
warn(start, "raw HTML block dropped" + (" (lines %d-%d)" % (start, end) if end > start else ""))
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
if stripped.startswith(">"):
|
|
247
|
+
flush_para()
|
|
248
|
+
inner = []
|
|
249
|
+
start = ln
|
|
250
|
+
while i < n:
|
|
251
|
+
l2 = expand_tabs(lines[i])
|
|
252
|
+
s2 = l2.lstrip(" ")
|
|
253
|
+
if s2.startswith(">"):
|
|
254
|
+
s2 = s2[1:]
|
|
255
|
+
if s2.startswith(" "):
|
|
256
|
+
s2 = s2[1:]
|
|
257
|
+
inner.append(s2)
|
|
258
|
+
elif l2.strip() and inner and inner[-1].strip():
|
|
259
|
+
inner.append(l2.strip())
|
|
260
|
+
else:
|
|
261
|
+
break
|
|
262
|
+
i += 1
|
|
263
|
+
blocks.append({"type": "quote", "blocks": parse_blocks(inner, start, refs), "line": start})
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
if "|" in line and i + 1 < n and RE_TABLE_SEP.match(lines[i + 1]) and "-" in lines[i + 1]:
|
|
267
|
+
flush_para()
|
|
268
|
+
header = split_row(line)
|
|
269
|
+
aligns = []
|
|
270
|
+
for c in split_row(lines[i + 1]):
|
|
271
|
+
c = c.strip()
|
|
272
|
+
if c.startswith(":") and c.endswith(":"):
|
|
273
|
+
aligns.append("ctr")
|
|
274
|
+
elif c.endswith(":"):
|
|
275
|
+
aligns.append("r")
|
|
276
|
+
else:
|
|
277
|
+
aligns.append("l")
|
|
278
|
+
rows = []
|
|
279
|
+
i += 2
|
|
280
|
+
while i < n and lines[i].strip() and "|" in lines[i]:
|
|
281
|
+
rows.append(split_row(lines[i]))
|
|
282
|
+
i += 1
|
|
283
|
+
width = len(header)
|
|
284
|
+
for r_i, r in enumerate(rows):
|
|
285
|
+
if len(r) != width:
|
|
286
|
+
warn(ln, "table row %d has %d cells, header has %d; padded or cut" % (r_i + 1, len(r), width))
|
|
287
|
+
rows = [(r + [""] * width)[:width] for r in rows]
|
|
288
|
+
aligns = (aligns + ["l"] * width)[:width]
|
|
289
|
+
blocks.append({"type": "table", "header": header, "rows": rows, "aligns": aligns, "line": ln})
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
m = RE_ITEM.match(line)
|
|
293
|
+
if m and (not para or m.group(3)):
|
|
294
|
+
marker = m.group(2)
|
|
295
|
+
if not para or not marker[0].isdigit() or int(marker[:-1]) == 1:
|
|
296
|
+
flush_para()
|
|
297
|
+
lst, i = parse_list(lines, i, base_line)
|
|
298
|
+
blocks.append(lst)
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
para.append((ln, line))
|
|
302
|
+
i += 1
|
|
303
|
+
|
|
304
|
+
flush_para()
|
|
305
|
+
return blocks
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def parse_list(lines, i, base_line):
|
|
309
|
+
"""A list block with items at levels 0-2. Deeper items are flattened to level 2."""
|
|
310
|
+
n = len(lines)
|
|
311
|
+
items = []
|
|
312
|
+
stack = [] # indent of each open level
|
|
313
|
+
deep_warned = False
|
|
314
|
+
start_ln = base_line + i
|
|
315
|
+
while i < n:
|
|
316
|
+
line = expand_tabs(lines[i])
|
|
317
|
+
ln = base_line + i
|
|
318
|
+
if not line.strip():
|
|
319
|
+
j = i + 1
|
|
320
|
+
while j < n and not lines[j].strip():
|
|
321
|
+
j += 1
|
|
322
|
+
if j < n:
|
|
323
|
+
nxt = expand_tabs(lines[j])
|
|
324
|
+
base = stack[0] if stack else 0
|
|
325
|
+
if RE_ITEM.match(nxt) and indent_of(nxt) >= base or (items and indent_of(nxt) >= base + 2):
|
|
326
|
+
i = j
|
|
327
|
+
continue
|
|
328
|
+
break
|
|
329
|
+
m = RE_ITEM.match(line)
|
|
330
|
+
ind = indent_of(line)
|
|
331
|
+
if m and not RE_HR.match(line):
|
|
332
|
+
if not stack:
|
|
333
|
+
stack = [ind]
|
|
334
|
+
if ind < stack[0]:
|
|
335
|
+
break
|
|
336
|
+
while len(stack) > 1 and ind < stack[-1]:
|
|
337
|
+
stack.pop()
|
|
338
|
+
if ind >= stack[-1] + 2:
|
|
339
|
+
stack.append(ind)
|
|
340
|
+
level = len(stack) - 1
|
|
341
|
+
if level > 2:
|
|
342
|
+
if not deep_warned:
|
|
343
|
+
warn(ln, "list nested deeper than three levels; flattened to the third level")
|
|
344
|
+
deep_warned = True
|
|
345
|
+
level = 2
|
|
346
|
+
marker = m.group(2)
|
|
347
|
+
ordered = marker[0].isdigit()
|
|
348
|
+
text = m.group(3) or ""
|
|
349
|
+
task = None
|
|
350
|
+
tm = re.match(r"^\[([ xX])\][ \t]+(.*)$", text)
|
|
351
|
+
if tm:
|
|
352
|
+
task = tm.group(1).lower() == "x"
|
|
353
|
+
text = tm.group(2)
|
|
354
|
+
items.append({"level": level, "ordered": ordered, "start": int(marker[:-1]) if ordered else 1,
|
|
355
|
+
"paren": marker.endswith(")"), "lines": [(ln, text)], "task": task})
|
|
356
|
+
i += 1
|
|
357
|
+
continue
|
|
358
|
+
base = stack[0] if stack else 0
|
|
359
|
+
if items and (ind >= base + 2 or not RE_ATX.match(line) and not line.lstrip().startswith(">")
|
|
360
|
+
and not RE_FENCE.match(line) and not RE_HR.match(line)
|
|
361
|
+
and not line.lstrip().startswith("<!--")):
|
|
362
|
+
s = line.strip()
|
|
363
|
+
if RE_FENCE.match(s) or s.startswith("|"):
|
|
364
|
+
warn(ln, "code block or table inside a list item; written as plain text in the item")
|
|
365
|
+
items[-1]["lines"].append((ln, s))
|
|
366
|
+
i += 1
|
|
367
|
+
continue
|
|
368
|
+
break
|
|
369
|
+
# number ordered items the way PowerPoint will: a run continues past deeper items
|
|
370
|
+
runs = [None, None, None]
|
|
371
|
+
run_id = 0
|
|
372
|
+
jump_warned = False
|
|
373
|
+
for it in items:
|
|
374
|
+
lvl = it["level"]
|
|
375
|
+
for k in range(lvl + 1, 3):
|
|
376
|
+
runs[k] = None
|
|
377
|
+
if it["ordered"]:
|
|
378
|
+
if runs[lvl] is None:
|
|
379
|
+
run_id += 1
|
|
380
|
+
runs[lvl] = [run_id, it["start"]]
|
|
381
|
+
elif it["start"] != runs[lvl][1] and not jump_warned:
|
|
382
|
+
warn(it["lines"][0][0], "numbered list goes from %d to %d in the source; the slides number items "
|
|
383
|
+
"in sequence, so \"%d.\" shows as %d. Check for a missing item, and for text that refers to "
|
|
384
|
+
"items by number" % (runs[lvl][1] - 1, it["start"], it["start"], runs[lvl][1]))
|
|
385
|
+
jump_warned = True
|
|
386
|
+
it["num"] = runs[lvl][1]
|
|
387
|
+
it["run"] = (start_ln, runs[lvl][0])
|
|
388
|
+
runs[lvl][1] += 1
|
|
389
|
+
else:
|
|
390
|
+
runs[lvl] = None
|
|
391
|
+
return {"type": "list", "items": items, "line": start_ln}, i
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# =========================================================================== inline parsing
|
|
395
|
+
|
|
396
|
+
RE_IMG = re.compile(r"!\[((?:[^\]\\]|\\.)*)\]\(\s*<?([^)\s>]+)>?(?:\s+[\"'][^\"']*[\"'])?\s*\)")
|
|
397
|
+
RE_LINK = re.compile(r"\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]\(\s*<?([^)\s>]*)>?(?:\s+[\"'][^\"']*[\"'])?\s*\)")
|
|
398
|
+
RE_REFLINK = re.compile(r"\[((?:[^\[\]\\]|\\.)+)\]\[([^\]]*)\]")
|
|
399
|
+
RE_SHORTREF = re.compile(r"\[((?:[^\[\]\\]|\\.)+)\](?![\[(:])")
|
|
400
|
+
RE_AUTOLINK = re.compile(r"<((?:https?|mailto|ftp):[^>\s]+)>")
|
|
401
|
+
RE_BAREURL = re.compile(r"https?://[^\s<>()]*[^\s<>().,;:!?'\"*_~]")
|
|
402
|
+
RE_TAG = re.compile(r"</?[A-Za-z][A-Za-z0-9-]*(\s[^<>]*)?/?>|<!--.*?-->")
|
|
403
|
+
RE_FOOTREF = re.compile(r"\[\^[^\]]+\]")
|
|
404
|
+
PUNCT = "\\`*_{}[]()#+-.!|~<>\"'$&"
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class Inline(object):
|
|
408
|
+
def __init__(self, refs, line):
|
|
409
|
+
self.refs = refs
|
|
410
|
+
self.line = line
|
|
411
|
+
self.html_warned = False
|
|
412
|
+
|
|
413
|
+
def parse(self, text, style=None):
|
|
414
|
+
"""Return segments: ('text', str, style) | ('br',) | ('img', alt, src)."""
|
|
415
|
+
style = dict(style or {})
|
|
416
|
+
out = []
|
|
417
|
+
buf = []
|
|
418
|
+
|
|
419
|
+
def flush():
|
|
420
|
+
if buf:
|
|
421
|
+
out.append(("text", "".join(buf), dict(style)))
|
|
422
|
+
del buf[:]
|
|
423
|
+
|
|
424
|
+
i, n = 0, len(text)
|
|
425
|
+
while i < n:
|
|
426
|
+
c = text[i]
|
|
427
|
+
rest = text[i:]
|
|
428
|
+
if c == "\\" and i + 1 < n and text[i + 1] in PUNCT:
|
|
429
|
+
buf.append(text[i + 1])
|
|
430
|
+
i += 2
|
|
431
|
+
continue
|
|
432
|
+
if c == "`":
|
|
433
|
+
run = len(rest) - len(rest.lstrip("`"))
|
|
434
|
+
close = text.find("`" * run, i + run)
|
|
435
|
+
while close != -1 and close + run < n and text[close + run] == "`":
|
|
436
|
+
close = text.find("`" * run, close + run + 1)
|
|
437
|
+
if close != -1:
|
|
438
|
+
flush()
|
|
439
|
+
code = text[i + run:close]
|
|
440
|
+
if code.startswith(" ") and code.endswith(" ") and code.strip():
|
|
441
|
+
code = code[1:-1]
|
|
442
|
+
st = dict(style)
|
|
443
|
+
st["code"] = True
|
|
444
|
+
out.append(("text", code, st))
|
|
445
|
+
i = close + run
|
|
446
|
+
continue
|
|
447
|
+
buf.append("`" * run)
|
|
448
|
+
i += run
|
|
449
|
+
continue
|
|
450
|
+
if c == "!" and rest.startswith("!["):
|
|
451
|
+
m = RE_IMG.match(rest)
|
|
452
|
+
if m:
|
|
453
|
+
flush()
|
|
454
|
+
out.append(("img", m.group(1), m.group(2)))
|
|
455
|
+
i += m.end()
|
|
456
|
+
continue
|
|
457
|
+
if c == "[":
|
|
458
|
+
m = RE_FOOTREF.match(rest)
|
|
459
|
+
if m:
|
|
460
|
+
warn(self.line, "footnote reference %s dropped" % m.group(0))
|
|
461
|
+
i += m.end()
|
|
462
|
+
continue
|
|
463
|
+
m = RE_LINK.match(rest)
|
|
464
|
+
url = None
|
|
465
|
+
if m:
|
|
466
|
+
label, url = m.group(1), m.group(2)
|
|
467
|
+
else:
|
|
468
|
+
m = RE_REFLINK.match(rest)
|
|
469
|
+
if m:
|
|
470
|
+
label = m.group(1)
|
|
471
|
+
key = (m.group(2) or m.group(1)).strip().lower()
|
|
472
|
+
url = self.refs.get(key)
|
|
473
|
+
if url is None:
|
|
474
|
+
m = None
|
|
475
|
+
if m is None:
|
|
476
|
+
m = RE_SHORTREF.match(rest)
|
|
477
|
+
if m and m.group(1).strip().lower() in self.refs:
|
|
478
|
+
label = m.group(1)
|
|
479
|
+
url = self.refs[label.strip().lower()]
|
|
480
|
+
else:
|
|
481
|
+
m = None
|
|
482
|
+
if m:
|
|
483
|
+
flush()
|
|
484
|
+
st = dict(style)
|
|
485
|
+
st["link"] = url
|
|
486
|
+
out.extend(self.parse(label, st))
|
|
487
|
+
i += m.end()
|
|
488
|
+
continue
|
|
489
|
+
if c == "<":
|
|
490
|
+
m = RE_AUTOLINK.match(rest)
|
|
491
|
+
if m:
|
|
492
|
+
flush()
|
|
493
|
+
st = dict(style)
|
|
494
|
+
st["link"] = m.group(1)
|
|
495
|
+
out.append(("text", m.group(1).replace("mailto:", ""), st))
|
|
496
|
+
i += m.end()
|
|
497
|
+
continue
|
|
498
|
+
m = RE_TAG.match(rest)
|
|
499
|
+
if m:
|
|
500
|
+
tag = m.group(0).lower()
|
|
501
|
+
if re.match(r"<br\s*/?>", tag):
|
|
502
|
+
flush()
|
|
503
|
+
out.append(("br",))
|
|
504
|
+
elif not self.html_warned:
|
|
505
|
+
warn(self.line, "inline HTML tag %s removed (text kept)" % m.group(0)[:30])
|
|
506
|
+
self.html_warned = True
|
|
507
|
+
i += m.end()
|
|
508
|
+
continue
|
|
509
|
+
if c == "h" and not style.get("link") and (i == 0 or not text[i - 1].isalnum()):
|
|
510
|
+
m = RE_BAREURL.match(rest)
|
|
511
|
+
if m:
|
|
512
|
+
flush()
|
|
513
|
+
st = dict(style)
|
|
514
|
+
st["link"] = m.group(0)
|
|
515
|
+
out.append(("text", m.group(0), st))
|
|
516
|
+
i += m.end()
|
|
517
|
+
continue
|
|
518
|
+
if c in "*_~":
|
|
519
|
+
run_char = c
|
|
520
|
+
run = len(rest) - len(rest.lstrip(c))
|
|
521
|
+
if c == "~" and run >= 2:
|
|
522
|
+
close = text.find("~~", i + 2)
|
|
523
|
+
if close > i + 2:
|
|
524
|
+
flush()
|
|
525
|
+
st = dict(style)
|
|
526
|
+
st["strike"] = True
|
|
527
|
+
out.extend(self.parse(text[i + 2:close], st))
|
|
528
|
+
i = close + 2
|
|
529
|
+
continue
|
|
530
|
+
elif c in "*_":
|
|
531
|
+
left_ok = i + run < n and not text[i + run].isspace()
|
|
532
|
+
if c == "_" and i > 0 and text[i - 1].isalnum():
|
|
533
|
+
left_ok = False
|
|
534
|
+
if left_ok:
|
|
535
|
+
for size in ((3, 2, 1) if run >= 3 else (2, 1) if run == 2 else (1,)):
|
|
536
|
+
delim = run_char * size
|
|
537
|
+
close = self._find_close(text, i + size, delim)
|
|
538
|
+
if close != -1:
|
|
539
|
+
flush()
|
|
540
|
+
st = dict(style)
|
|
541
|
+
if size >= 2:
|
|
542
|
+
st["bold"] = True
|
|
543
|
+
if size in (1, 3):
|
|
544
|
+
st["italic"] = True
|
|
545
|
+
out.extend(self.parse(text[i + size:close], st))
|
|
546
|
+
i = close + size
|
|
547
|
+
break
|
|
548
|
+
else:
|
|
549
|
+
buf.append(c * run)
|
|
550
|
+
i += run
|
|
551
|
+
continue
|
|
552
|
+
buf.append(c * run)
|
|
553
|
+
i += run
|
|
554
|
+
continue
|
|
555
|
+
if c == "\n":
|
|
556
|
+
if buf and ("".join(buf).endswith(" ") or "".join(buf).endswith("\\")):
|
|
557
|
+
s = "".join(buf).rstrip(" ").rstrip("\\")
|
|
558
|
+
del buf[:]
|
|
559
|
+
buf.append(s)
|
|
560
|
+
flush()
|
|
561
|
+
out.append(("br",))
|
|
562
|
+
else:
|
|
563
|
+
s = "".join(buf).rstrip(" ")
|
|
564
|
+
del buf[:]
|
|
565
|
+
buf.append(s + " ")
|
|
566
|
+
i += 1
|
|
567
|
+
while i < n and text[i] == " ":
|
|
568
|
+
i += 1
|
|
569
|
+
continue
|
|
570
|
+
buf.append(c)
|
|
571
|
+
i += 1
|
|
572
|
+
flush()
|
|
573
|
+
return out
|
|
574
|
+
|
|
575
|
+
@staticmethod
|
|
576
|
+
def _find_close(text, start, delim):
|
|
577
|
+
ch, size, n = delim[0], len(delim), len(text)
|
|
578
|
+
pos = start
|
|
579
|
+
while True:
|
|
580
|
+
close = text.find(delim, pos)
|
|
581
|
+
if close == -1:
|
|
582
|
+
return -1
|
|
583
|
+
run_end = close
|
|
584
|
+
while run_end < n and text[run_end] == ch:
|
|
585
|
+
run_end += 1
|
|
586
|
+
run = run_end - close
|
|
587
|
+
if close == start or text[close - 1].isspace():
|
|
588
|
+
pos = run_end
|
|
589
|
+
continue
|
|
590
|
+
if ch == "_" and run_end < n and text[run_end].isalnum():
|
|
591
|
+
pos = run_end
|
|
592
|
+
continue
|
|
593
|
+
if run == size:
|
|
594
|
+
return close
|
|
595
|
+
if run == 3 and size < 3:
|
|
596
|
+
return run_end - size
|
|
597
|
+
pos = run_end
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def plain(segs):
|
|
601
|
+
out = []
|
|
602
|
+
for s in segs:
|
|
603
|
+
if s[0] == "text":
|
|
604
|
+
out.append(s[1])
|
|
605
|
+
elif s[0] == "br":
|
|
606
|
+
out.append("\n")
|
|
607
|
+
elif s[0] == "img":
|
|
608
|
+
out.append("[%s]" % s[1])
|
|
609
|
+
return "".join(out)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def slugify(text):
|
|
613
|
+
text = re.sub(r"[*_`\[\]]|\(.*?\)", "", text)
|
|
614
|
+
return re.sub(r"[^\w\- ]", "", text.lower()).strip().replace(" ", "-")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# =========================================================================== images
|
|
618
|
+
|
|
619
|
+
def image_size(path):
|
|
620
|
+
"""Return (width_px, height_px, dpi_x, dpi_y, kind) or None."""
|
|
621
|
+
with open(path, "rb") as f:
|
|
622
|
+
data = f.read()
|
|
623
|
+
if data[:8] == b"\x89PNG\r\n\x1a\n" and len(data) >= 24:
|
|
624
|
+
w, h = struct.unpack(">II", data[16:24])
|
|
625
|
+
dx = dy = 96.0
|
|
626
|
+
pos = 8
|
|
627
|
+
while pos + 8 <= len(data):
|
|
628
|
+
length, ctype = struct.unpack(">I4s", data[pos:pos + 8])
|
|
629
|
+
if ctype == b"pHYs" and length == 9:
|
|
630
|
+
px, py, unit = struct.unpack(">IIB", data[pos + 8:pos + 17])
|
|
631
|
+
if unit == 1 and px and py:
|
|
632
|
+
dx, dy = px * 0.0254, py * 0.0254
|
|
633
|
+
break
|
|
634
|
+
if ctype == b"IDAT":
|
|
635
|
+
break
|
|
636
|
+
pos += 12 + length
|
|
637
|
+
return (w, h, dx, dy, "png") if w and h else None
|
|
638
|
+
if data[:2] == b"\xff\xd8":
|
|
639
|
+
dx = dy = 96.0
|
|
640
|
+
pos = 2
|
|
641
|
+
while pos + 4 <= len(data):
|
|
642
|
+
if data[pos] != 0xFF:
|
|
643
|
+
pos += 1
|
|
644
|
+
continue
|
|
645
|
+
marker = data[pos + 1]
|
|
646
|
+
if marker in (0xD8, 0x01, 0xFF) or 0xD0 <= marker <= 0xD7:
|
|
647
|
+
pos += 1 if marker == 0xFF else 2
|
|
648
|
+
continue
|
|
649
|
+
length = struct.unpack(">H", data[pos + 2:pos + 4])[0]
|
|
650
|
+
seg = data[pos + 4:pos + 2 + length]
|
|
651
|
+
if marker == 0xE0 and seg[:5] == b"JFIF\x00" and len(seg) >= 12:
|
|
652
|
+
units = seg[7]
|
|
653
|
+
xd, yd = struct.unpack(">HH", seg[8:12])
|
|
654
|
+
if units == 1 and xd and yd:
|
|
655
|
+
dx, dy = float(xd), float(yd)
|
|
656
|
+
elif units == 2 and xd and yd:
|
|
657
|
+
dx, dy = xd * 2.54, yd * 2.54
|
|
658
|
+
if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC) and len(seg) >= 5:
|
|
659
|
+
h, w = struct.unpack(">HH", seg[1:5])
|
|
660
|
+
return (w, h, dx, dy, "jpeg") if w and h else None
|
|
661
|
+
pos += 2 + length
|
|
662
|
+
return None
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# =========================================================================== geometry
|
|
666
|
+
|
|
667
|
+
EMU_IN = 914400
|
|
668
|
+
EMU_PT = 12700
|
|
669
|
+
SLIDE_H = 6858000
|
|
670
|
+
SLIDE_W = {"16:9": 12192000, "4:3": 9144000}
|
|
671
|
+
MARGIN_X = int(0.6 * EMU_IN)
|
|
672
|
+
TITLE_Y = int(0.35 * EMU_IN)
|
|
673
|
+
TITLE_H = int(1.0 * EMU_IN)
|
|
674
|
+
BODY_Y = int(1.45 * EMU_IN)
|
|
675
|
+
BODY_BOTTOM = int(7.05 * EMU_IN)
|
|
676
|
+
GAP = int(0.12 * EMU_IN)
|
|
677
|
+
INSET = 45720 # 0.05 in top and bottom inset of text frames
|
|
678
|
+
SIDE_INSET = 91440 # 0.1 in left and right
|
|
679
|
+
USABLE = 0.96 # keep a little headroom: the height model is an estimate
|
|
680
|
+
|
|
681
|
+
TITLE_PT = 32
|
|
682
|
+
SIZES = { # role -> (font pt, left margin in, hanging indent in, space before pt)
|
|
683
|
+
"p": (20, 0.0, 0.0, 8),
|
|
684
|
+
"quote": (20, 0.4, 0.0, 8),
|
|
685
|
+
"sub": (22, 0.0, 0.0, 12),
|
|
686
|
+
0: (20, 0.375, 0.375, 8),
|
|
687
|
+
1: (18, 0.75, 0.3, 4),
|
|
688
|
+
2: (16, 1.1, 0.25, 2),
|
|
689
|
+
}
|
|
690
|
+
CODE_PT = 14
|
|
691
|
+
TABLE_PT = 14
|
|
692
|
+
CELL_MAR_X = 91440
|
|
693
|
+
CELL_MAR_Y = 45720
|
|
694
|
+
BULLETS = {0: u"\u2022", 1: u"\u2013", 2: u"\u25e6"}
|
|
695
|
+
NUM_SCHEMES = {0: "arabic", 1: "alphaLc", 2: "romanLc"}
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def char_factor(font):
|
|
699
|
+
f = (font or "").lower()
|
|
700
|
+
if any(k in f for k in ("verdana", "tahoma", "trebuchet")):
|
|
701
|
+
return 0.60
|
|
702
|
+
if any(k in f for k in ("calibri", "aptos", "segoe", "candara", "corbel")):
|
|
703
|
+
return 0.50
|
|
704
|
+
return 0.55
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def wrapped_lines(text, cpl):
|
|
708
|
+
total = 0
|
|
709
|
+
for hard in text.split("\n"):
|
|
710
|
+
words = hard.split()
|
|
711
|
+
if not words:
|
|
712
|
+
total += 1
|
|
713
|
+
continue
|
|
714
|
+
lines, cur = 1, 0
|
|
715
|
+
for w in words:
|
|
716
|
+
wl = len(w)
|
|
717
|
+
while wl > cpl: # a very long token (URL) breaks mid-word
|
|
718
|
+
if cur:
|
|
719
|
+
lines += 1
|
|
720
|
+
lines += wl // cpl - (1 if cur == 0 else 0)
|
|
721
|
+
wl = wl % cpl
|
|
722
|
+
cur = 0
|
|
723
|
+
need = wl + (1 if cur else 0)
|
|
724
|
+
if cur + need > cpl:
|
|
725
|
+
lines += 1
|
|
726
|
+
cur = wl
|
|
727
|
+
else:
|
|
728
|
+
cur += need
|
|
729
|
+
total += lines
|
|
730
|
+
return total
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def text_lines(text, size_pt, width_emu, factor):
|
|
734
|
+
width_pt = width_emu / float(EMU_PT)
|
|
735
|
+
cpl = max(6, int(width_pt / (size_pt * factor)))
|
|
736
|
+
return wrapped_lines(text, cpl)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
# =========================================================================== deck model
|
|
740
|
+
|
|
741
|
+
class Deck(object):
|
|
742
|
+
def __init__(self, src_dir, aspect, brand, refs):
|
|
743
|
+
self.src_dir = src_dir
|
|
744
|
+
self.aspect = aspect
|
|
745
|
+
self.brand = brand
|
|
746
|
+
self.refs = refs
|
|
747
|
+
self.width = SLIDE_W[aspect]
|
|
748
|
+
self.body_w = self.width - 2 * MARGIN_X
|
|
749
|
+
self.body_h = int((BODY_BOTTOM - BODY_Y) * USABLE)
|
|
750
|
+
self.body_factor = char_factor(brand["body_font"])
|
|
751
|
+
self.head_factor = char_factor(brand["heading_font"])
|
|
752
|
+
self.media = []
|
|
753
|
+
self.media_by_path = {}
|
|
754
|
+
self.counts = {"tables": 0, "images": 0, "links": 0, "code blocks": 0}
|
|
755
|
+
|
|
756
|
+
# ---- unit builders ------------------------------------------------------------
|
|
757
|
+
def inline(self, text, line, style=None):
|
|
758
|
+
return Inline(self.refs, line).parse(text, style)
|
|
759
|
+
|
|
760
|
+
def para_unit(self, role, segs, line, level=None, extra=None):
|
|
761
|
+
key = level if role in ("bullet", "num") else role
|
|
762
|
+
size, marl, _, spc = SIZES[key]
|
|
763
|
+
width = self.body_w - 2 * SIDE_INSET - int(marl * EMU_IN)
|
|
764
|
+
factor = self.head_factor * 1.06 if role == "sub" else self.body_factor
|
|
765
|
+
if any(s[0] == "text" and s[2].get("bold") for s in segs):
|
|
766
|
+
factor *= 1.03
|
|
767
|
+
lines = text_lines(plain(segs), size, width, factor)
|
|
768
|
+
h = int((lines * size * 1.2 + spc) * EMU_PT)
|
|
769
|
+
u = {"kind": "para", "role": role, "level": level or 0, "segs": segs, "line": line, "h": h}
|
|
770
|
+
if extra:
|
|
771
|
+
u.update(extra)
|
|
772
|
+
return u
|
|
773
|
+
|
|
774
|
+
def image_unit(self, alt, src, line):
|
|
775
|
+
alt_plain = re.sub(r"\\(.)", r"\1", alt)
|
|
776
|
+
fallback = [("text", "[%s]" % (alt_plain or "image"), {"italic": True})]
|
|
777
|
+
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", src) or src.startswith("data:"):
|
|
778
|
+
warn(line, "remote or inline image %s not fetched; alt text written instead" % src[:60])
|
|
779
|
+
return self.para_unit("p", fallback, line)
|
|
780
|
+
path = os.path.join(self.src_dir, src.replace("%20", " "))
|
|
781
|
+
if not os.path.isfile(path):
|
|
782
|
+
warn(line, "image %s not found; alt text written instead" % src)
|
|
783
|
+
return self.para_unit("p", fallback, line)
|
|
784
|
+
info = image_size(path)
|
|
785
|
+
if not info:
|
|
786
|
+
warn(line, "image %s is not PNG or JPEG; alt text written instead" % src)
|
|
787
|
+
return self.para_unit("p", fallback, line)
|
|
788
|
+
w, h, dx, dy, kind = info
|
|
789
|
+
cx, cy = w / dx * EMU_IN, h / dy * EMU_IN
|
|
790
|
+
scale = min(1.0, self.body_w / cx, self.body_h / cy)
|
|
791
|
+
return {"kind": "image", "path": os.path.abspath(path), "alt": alt_plain, "fmt": kind,
|
|
792
|
+
"cx": int(cx * scale), "cy": int(cy * scale), "line": line}
|
|
793
|
+
|
|
794
|
+
def table_rows(self, header, rows, aligns):
|
|
795
|
+
QUIET[0] = True
|
|
796
|
+
try:
|
|
797
|
+
return self._table_rows(header, rows)
|
|
798
|
+
finally:
|
|
799
|
+
QUIET[0] = False
|
|
800
|
+
|
|
801
|
+
def _table_rows(self, header, rows):
|
|
802
|
+
ncols = len(header)
|
|
803
|
+
natural = []
|
|
804
|
+
for c in range(ncols):
|
|
805
|
+
longest = max([len(plain(self.inline(r[c], 0))) for r in [header] + rows] + [3])
|
|
806
|
+
natural.append((min(longest, 40) + 1) * TABLE_PT * 0.6 * EMU_PT + 2 * CELL_MAR_X)
|
|
807
|
+
total = sum(natural)
|
|
808
|
+
width = int(min(self.body_w, max(total, self.body_w * 0.5)))
|
|
809
|
+
cols = [max(int(width * nw / total), int(0.75 * EMU_IN)) for nw in natural]
|
|
810
|
+
width = sum(cols)
|
|
811
|
+
if width > self.body_w:
|
|
812
|
+
cols = [int(c * self.body_w / float(width)) for c in cols]
|
|
813
|
+
width = sum(cols)
|
|
814
|
+
heights = []
|
|
815
|
+
for r in [header] + rows:
|
|
816
|
+
lines = 1
|
|
817
|
+
for c in range(ncols):
|
|
818
|
+
f = self.body_factor * (1.05 if r is header else 1.0)
|
|
819
|
+
lines = max(lines, text_lines(plain(self.inline(r[c], 0)), TABLE_PT,
|
|
820
|
+
cols[c] - 2 * CELL_MAR_X, f))
|
|
821
|
+
heights.append(int(lines * TABLE_PT * 1.2 * EMU_PT + 2 * CELL_MAR_Y + 2 * EMU_PT))
|
|
822
|
+
return cols, heights
|
|
823
|
+
|
|
824
|
+
def code_lines_h(self, lines):
|
|
825
|
+
width = self.body_w - 2 * SIDE_INSET
|
|
826
|
+
n = sum(text_lines(l or " ", CODE_PT, width, 0.6) for l in lines)
|
|
827
|
+
return int(n * CODE_PT * 1.2 * EMU_PT) + 2 * int(0.08 * EMU_IN)
|
|
828
|
+
|
|
829
|
+
def units_from_blocks(self, blocks, quote=False):
|
|
830
|
+
units = []
|
|
831
|
+
for b in blocks:
|
|
832
|
+
t, ln = b["type"], b["line"]
|
|
833
|
+
if t == "heading":
|
|
834
|
+
segs = self.inline(b["text"], ln, {"bold": True})
|
|
835
|
+
units.append(self.para_unit("sub", segs, ln, extra={"slug": slugify(b["text"])}))
|
|
836
|
+
elif t == "para":
|
|
837
|
+
text = "\n".join(l for _, l in b["lines"]).strip()
|
|
838
|
+
if RE_NOTES_PARA.match(text):
|
|
839
|
+
warn(ln, "paragraph starting \"Notes:\" stays on the slide as text; wrap it in "
|
|
840
|
+
"<!-- notes: ... --> to make it speaker notes")
|
|
841
|
+
segs = self.inline(text, ln)
|
|
842
|
+
imgs = [s for s in segs if s[0] == "img"]
|
|
843
|
+
rest = [s for s in segs if s[0] != "img"]
|
|
844
|
+
if plain(rest).strip():
|
|
845
|
+
if imgs:
|
|
846
|
+
warn(ln, "image inside a text paragraph placed below the paragraph")
|
|
847
|
+
units.append(self.para_unit("quote" if quote else "p", rest, ln))
|
|
848
|
+
for s in imgs:
|
|
849
|
+
units.append(self.image_unit(s[1], s[2], ln))
|
|
850
|
+
elif t == "list":
|
|
851
|
+
for it in b["items"]:
|
|
852
|
+
iln = it["lines"][0][0]
|
|
853
|
+
segs = self.inline("\n".join(tx for _, tx in it["lines"]), iln)
|
|
854
|
+
if any(s[0] == "img" for s in segs):
|
|
855
|
+
warn(iln, "image inside a list item not embedded; alt text written instead")
|
|
856
|
+
if it["task"] is not None:
|
|
857
|
+
segs.insert(0, ("text", u"\u2612 " if it["task"] else u"\u2610 ", {}))
|
|
858
|
+
role = "num" if it["ordered"] else "bullet"
|
|
859
|
+
extra = {"num": it.get("num"), "run": it.get("run"), "paren": it["paren"]}
|
|
860
|
+
units.append(self.para_unit(role, segs, iln, it["level"], extra))
|
|
861
|
+
elif t == "code":
|
|
862
|
+
self.counts["code blocks"] += 1
|
|
863
|
+
units.append({"kind": "code", "lines": b["text"].split("\n"), "line": ln})
|
|
864
|
+
elif t == "quote":
|
|
865
|
+
units.extend(self.units_from_blocks(b["blocks"], quote=True))
|
|
866
|
+
elif t == "table":
|
|
867
|
+
self.counts["tables"] += 1
|
|
868
|
+
if len(b["header"]) > 8:
|
|
869
|
+
warn(ln, "table has %d columns; at 14 pt the cells wrap heavily. Split it into two "
|
|
870
|
+
"tables or move it to an appendix" % len(b["header"]))
|
|
871
|
+
cols, heights = self.table_rows(b["header"], b["rows"], b["aligns"])
|
|
872
|
+
units.append({"kind": "table", "header": b["header"], "rows": b["rows"], "aligns": b["aligns"],
|
|
873
|
+
"cols": cols, "heights": heights, "line": ln})
|
|
874
|
+
elif t == "notes":
|
|
875
|
+
units.append({"kind": "notes", "text": b["text"], "line": ln})
|
|
876
|
+
elif t == "hr":
|
|
877
|
+
units.append({"kind": "break", "line": ln})
|
|
878
|
+
return units
|
|
879
|
+
|
|
880
|
+
# ---- pagination ---------------------------------------------------------------
|
|
881
|
+
def paginate(self, units, title, line):
|
|
882
|
+
"""Pack units into pages of body height. Returns (pages, split_reason or None)."""
|
|
883
|
+
H = self.body_h
|
|
884
|
+
pages = [[]]
|
|
885
|
+
used = [0]
|
|
886
|
+
last = [None]
|
|
887
|
+
reasons = []
|
|
888
|
+
|
|
889
|
+
def new_page():
|
|
890
|
+
if pages[-1] and any(u["kind"] != "notes" for u in pages[-1]):
|
|
891
|
+
pages.append([])
|
|
892
|
+
used[0] = 0
|
|
893
|
+
last[0] = None
|
|
894
|
+
|
|
895
|
+
def cost(u, h):
|
|
896
|
+
if u["kind"] == "para" and last[0] == "para":
|
|
897
|
+
return h
|
|
898
|
+
return (GAP if used[0] else 0) + (2 * INSET if u["kind"] == "para" else 0) + h
|
|
899
|
+
|
|
900
|
+
def place(u, h):
|
|
901
|
+
used[0] += cost(u, h)
|
|
902
|
+
last[0] = u["kind"]
|
|
903
|
+
pages[-1].append(u)
|
|
904
|
+
|
|
905
|
+
for u in units:
|
|
906
|
+
k = u["kind"]
|
|
907
|
+
if k == "notes":
|
|
908
|
+
pages[-1].append(u)
|
|
909
|
+
continue
|
|
910
|
+
if k == "break":
|
|
911
|
+
new_page()
|
|
912
|
+
continue
|
|
913
|
+
if k == "para":
|
|
914
|
+
if used[0] + cost(u, u["h"]) > H and used[0]:
|
|
915
|
+
carry = []
|
|
916
|
+
# keep a subheading, or a lead-in line ending in ":", with what follows it
|
|
917
|
+
while pages[-1] and pages[-1][-1]["kind"] == "para" and (
|
|
918
|
+
pages[-1][-1]["role"] == "sub" or plain(pages[-1][-1]["segs"]).rstrip().endswith(":")) \
|
|
919
|
+
and len([p for p in pages[-1] if p["kind"] != "notes"]) > 1:
|
|
920
|
+
carry.insert(0, pages[-1].pop())
|
|
921
|
+
reasons.append("text")
|
|
922
|
+
pages.append([])
|
|
923
|
+
used[0] = 0
|
|
924
|
+
last[0] = None
|
|
925
|
+
for c in carry:
|
|
926
|
+
place(c, c["h"])
|
|
927
|
+
if not used[0] and cost(u, u["h"]) > H:
|
|
928
|
+
warn(u["line"], "one paragraph is taller than a slide and will overflow it; "
|
|
929
|
+
"break it into bullets or shorten it")
|
|
930
|
+
place(u, u["h"])
|
|
931
|
+
elif k == "image":
|
|
932
|
+
full = u["cy"]
|
|
933
|
+
room = H - used[0] - (GAP if used[0] else 0)
|
|
934
|
+
if room >= full:
|
|
935
|
+
place(u, full)
|
|
936
|
+
elif used[0] and room >= max(0.6 * full, 1.5 * EMU_IN):
|
|
937
|
+
s = room / float(full)
|
|
938
|
+
u["cx"], u["cy"] = int(u["cx"] * s), int(u["cy"] * s)
|
|
939
|
+
place(u, u["cy"])
|
|
940
|
+
else:
|
|
941
|
+
reasons.append("image")
|
|
942
|
+
new_page()
|
|
943
|
+
place(u, full)
|
|
944
|
+
elif k == "code":
|
|
945
|
+
lines = u["lines"]
|
|
946
|
+
while lines:
|
|
947
|
+
room = H - used[0] - (GAP if used[0] else 0)
|
|
948
|
+
take = len(lines)
|
|
949
|
+
while take > 0 and self.code_lines_h(lines[:take]) > room:
|
|
950
|
+
take -= 1
|
|
951
|
+
if take == len(lines) or (take >= 3 and used[0]) or not used[0]:
|
|
952
|
+
if take == 0:
|
|
953
|
+
take = 1
|
|
954
|
+
left = len(lines) - take
|
|
955
|
+
if 0 < left < 3 and take > 6: # don't strand one or two lines on a slide
|
|
956
|
+
take -= 3 - left
|
|
957
|
+
chunk = dict(u, lines=lines[:take])
|
|
958
|
+
place(chunk, self.code_lines_h(chunk["lines"]))
|
|
959
|
+
lines = lines[take:]
|
|
960
|
+
if lines:
|
|
961
|
+
reasons.append("code")
|
|
962
|
+
new_page()
|
|
963
|
+
else:
|
|
964
|
+
reasons.append("code")
|
|
965
|
+
new_page()
|
|
966
|
+
elif k == "table":
|
|
967
|
+
rows = list(range(len(u["rows"])))
|
|
968
|
+
hh = u["heights"][0]
|
|
969
|
+
first = True
|
|
970
|
+
while first or rows:
|
|
971
|
+
first = False
|
|
972
|
+
room = H - used[0] - (GAP if used[0] else 0)
|
|
973
|
+
take, h = 0, hh
|
|
974
|
+
while take < len(rows) and h + u["heights"][rows[take] + 1] <= room:
|
|
975
|
+
h += u["heights"][rows[take] + 1]
|
|
976
|
+
take += 1
|
|
977
|
+
if take == len(rows) or (take >= 2 and used[0]) or not used[0]:
|
|
978
|
+
if take == 0 and rows:
|
|
979
|
+
take = 1
|
|
980
|
+
left = len(rows) - take
|
|
981
|
+
if left == 1 and take > 4: # don't strand one row under a repeated header
|
|
982
|
+
take -= 1
|
|
983
|
+
h = hh + sum(u["heights"][r + 1] for r in rows[:take])
|
|
984
|
+
chunk = dict(u, rows=[u["rows"][r] for r in rows[:take]],
|
|
985
|
+
heights=[hh] + [u["heights"][r + 1] for r in rows[:take]])
|
|
986
|
+
if h > H:
|
|
987
|
+
warn(u["line"], "a table row is taller than a slide and will overflow it")
|
|
988
|
+
place(chunk, h)
|
|
989
|
+
rows = rows[take:]
|
|
990
|
+
if rows:
|
|
991
|
+
reasons.append("table")
|
|
992
|
+
new_page()
|
|
993
|
+
else:
|
|
994
|
+
reasons.append("table")
|
|
995
|
+
new_page()
|
|
996
|
+
if not pages[-1] and len(pages) > 1:
|
|
997
|
+
pages.pop()
|
|
998
|
+
return pages, reasons
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
# =========================================================================== XML writers
|
|
1002
|
+
|
|
1003
|
+
NS = ('xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
|
1004
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
|
|
1005
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"')
|
|
1006
|
+
XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
|
|
1007
|
+
REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/"
|
|
1008
|
+
CT = "application/vnd.openxmlformats-officedocument.presentationml."
|
|
1009
|
+
GRP = ('<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
|
|
1010
|
+
'<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/>'
|
|
1011
|
+
'<a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>')
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def xfrm(xo, yo, cx, cy):
|
|
1015
|
+
return '<a:xfrm><a:off x="%d" y="%d"/><a:ext cx="%d" cy="%d"/></a:xfrm>' % (xo, yo, cx, cy)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def rels_xml(rels):
|
|
1019
|
+
rows = []
|
|
1020
|
+
for rid, kind, target, ext in rels:
|
|
1021
|
+
rows.append('<Relationship Id="%s" Type="%s%s" Target="%s"%s/>' % (
|
|
1022
|
+
rid, REL, kind, x(target), ' TargetMode="External"' if ext else ""))
|
|
1023
|
+
return (XML_HEAD + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
1024
|
+
'%s</Relationships>') % "".join(rows)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
class SlideOut(object):
|
|
1028
|
+
def __init__(self, deck, layout, anchors, number):
|
|
1029
|
+
self.deck = deck
|
|
1030
|
+
self.rels = [("rId1", "slideLayout", "../slideLayouts/slideLayout%d.xml" % layout, False)]
|
|
1031
|
+
self.shapes = []
|
|
1032
|
+
self.next_id = 2
|
|
1033
|
+
self.anchors = anchors
|
|
1034
|
+
self.number = number
|
|
1035
|
+
self.notes = []
|
|
1036
|
+
self.layout = layout
|
|
1037
|
+
|
|
1038
|
+
def add_rel(self, kind, target, ext=False):
|
|
1039
|
+
for rid, k, t, e in self.rels:
|
|
1040
|
+
if k == kind and t == target and e == ext:
|
|
1041
|
+
return rid
|
|
1042
|
+
rid = "rId%d" % (len(self.rels) + 1)
|
|
1043
|
+
self.rels.append((rid, kind, target, ext))
|
|
1044
|
+
return rid
|
|
1045
|
+
|
|
1046
|
+
def sid(self):
|
|
1047
|
+
i = self.next_id
|
|
1048
|
+
self.next_id += 1
|
|
1049
|
+
return i
|
|
1050
|
+
|
|
1051
|
+
# ---- runs
|
|
1052
|
+
def runs(self, segs, size, line, color=None, font=None):
|
|
1053
|
+
out = []
|
|
1054
|
+
sz = ' sz="%d"' % int(size * 100) if size else ""
|
|
1055
|
+
for seg in segs:
|
|
1056
|
+
if seg[0] == "br":
|
|
1057
|
+
out.append('<a:br><a:rPr lang="en-US"%s/></a:br>' % sz)
|
|
1058
|
+
continue
|
|
1059
|
+
if seg[0] == "img":
|
|
1060
|
+
warn(line, "image inside a list item or table cell not embedded; alt text written instead")
|
|
1061
|
+
seg = ("text", "[%s]" % (seg[1] or "image"), {"italic": True})
|
|
1062
|
+
_, text, st = seg
|
|
1063
|
+
if not text:
|
|
1064
|
+
continue
|
|
1065
|
+
attrs = ' lang="en-US"' + sz
|
|
1066
|
+
if st.get("bold"):
|
|
1067
|
+
attrs += ' b="1"'
|
|
1068
|
+
if st.get("italic"):
|
|
1069
|
+
attrs += ' i="1"'
|
|
1070
|
+
if st.get("strike"):
|
|
1071
|
+
attrs += ' strike="sngStrike"'
|
|
1072
|
+
kids = ""
|
|
1073
|
+
if color:
|
|
1074
|
+
kids += '<a:solidFill><a:srgbClr val="%s"/></a:solidFill>' % color
|
|
1075
|
+
if st.get("code"):
|
|
1076
|
+
kids += '<a:latin typeface="Consolas" pitchFamily="49" charset="0"/>' \
|
|
1077
|
+
'<a:cs typeface="Consolas" pitchFamily="49" charset="0"/>'
|
|
1078
|
+
elif font:
|
|
1079
|
+
kids += '<a:latin typeface="%s"/>' % x(font)
|
|
1080
|
+
link = st.get("link")
|
|
1081
|
+
if link is not None:
|
|
1082
|
+
kids += self.link_xml(link, line)
|
|
1083
|
+
out.append('<a:r><a:rPr%s>%s</a:rPr><a:t>%s</a:t></a:r>' % (attrs, kids, x(text)))
|
|
1084
|
+
return "".join(out)
|
|
1085
|
+
|
|
1086
|
+
def link_xml(self, url, line):
|
|
1087
|
+
if not url:
|
|
1088
|
+
return ""
|
|
1089
|
+
if url.startswith("#"):
|
|
1090
|
+
target = self.anchors.get(url[1:].lower())
|
|
1091
|
+
if not target:
|
|
1092
|
+
warn(line, "link to #%s has no matching heading; kept as plain text" % url[1:])
|
|
1093
|
+
return ""
|
|
1094
|
+
self.deck.counts["links"] += 1
|
|
1095
|
+
rid = self.add_rel("slide", "slide%d.xml" % target)
|
|
1096
|
+
return '<a:hlinkClick r:id="%s" action="ppaction://hlinksldjump"/>' % rid
|
|
1097
|
+
if not re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:", url):
|
|
1098
|
+
warn(line, "link to local file %s kept as a link, but the file is not inside the deck; "
|
|
1099
|
+
"it opens only where that path exists" % url[:60])
|
|
1100
|
+
self.deck.counts["links"] += 1
|
|
1101
|
+
rid = self.add_rel("hyperlink", quote(url, safe=":/?#[]@!$&'()*+,;=%~-._"), ext=True)
|
|
1102
|
+
return '<a:hlinkClick r:id="%s"/>' % rid
|
|
1103
|
+
|
|
1104
|
+
# ---- shapes
|
|
1105
|
+
def title(self, segs, line, ctr=False, sub_lines=None):
|
|
1106
|
+
ph = '<p:ph type="ctrTitle"/>' if ctr else '<p:ph type="title"/>'
|
|
1107
|
+
body = self.runs(segs, None, line)
|
|
1108
|
+
self.shapes.append(
|
|
1109
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Title %d"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>'
|
|
1110
|
+
'<p:nvPr>%s</p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p>%s'
|
|
1111
|
+
'<a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp>' % (self.sid(), self.next_id - 2, ph, body))
|
|
1112
|
+
if sub_lines:
|
|
1113
|
+
paras = "".join('<a:p>%s</a:p>' % self.runs(s, None, line) for s in sub_lines)
|
|
1114
|
+
self.shapes.append(
|
|
1115
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Subtitle %d"/><p:cNvSpPr><a:spLocks noGrp="1"/>'
|
|
1116
|
+
'</p:cNvSpPr><p:nvPr><p:ph type="subTitle" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/>'
|
|
1117
|
+
'<p:txBody><a:bodyPr/><a:lstStyle/>%s</p:txBody></p:sp>' % (self.sid(), self.next_id - 2, paras))
|
|
1118
|
+
|
|
1119
|
+
def para_xml(self, u, startat):
|
|
1120
|
+
role, lvl = u["role"], u["level"]
|
|
1121
|
+
key = lvl if role in ("bullet", "num") else role
|
|
1122
|
+
size, marl, hang, spc = SIZES[key]
|
|
1123
|
+
color = None
|
|
1124
|
+
attrs = ' marL="%d" indent="%d"' % (int(marl * EMU_IN), -int(hang * EMU_IN))
|
|
1125
|
+
if role in ("bullet", "num"):
|
|
1126
|
+
attrs += ' lvl="%d"' % lvl
|
|
1127
|
+
inner = '<a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:spcBef><a:spcPts val="%d"/></a:spcBef>' % (spc * 100)
|
|
1128
|
+
if role == "bullet":
|
|
1129
|
+
inner += ('<a:buClr><a:schemeClr val="accent1"/></a:buClr><a:buFont typeface="Arial"/>'
|
|
1130
|
+
'<a:buChar char="%s"/>' % BULLETS[lvl])
|
|
1131
|
+
elif role == "num":
|
|
1132
|
+
scheme = NUM_SCHEMES[lvl] + ("ParenR" if u.get("paren") else "Period")
|
|
1133
|
+
inner += '<a:buClr><a:schemeClr val="accent1"/></a:buClr><a:buFont typeface="+mj-lt"/>' \
|
|
1134
|
+
'<a:buAutoNum type="%s"%s/>' % (scheme, ' startAt="%d"' % startat if startat != 1 else "")
|
|
1135
|
+
else:
|
|
1136
|
+
inner += '<a:buNone/>'
|
|
1137
|
+
segs = u["segs"]
|
|
1138
|
+
if role == "quote":
|
|
1139
|
+
segs = [(s[0], s[1], dict(s[2], italic=True)) if s[0] == "text" else s for s in segs]
|
|
1140
|
+
color = "595959"
|
|
1141
|
+
if role == "sub":
|
|
1142
|
+
color = self.deck.brand.get("primary_for_text", self.deck.brand["primary"])
|
|
1143
|
+
font = self.deck.brand["heading_font"] if role == "sub" else None
|
|
1144
|
+
return '<a:p><a:pPr%s>%s</a:pPr>%s<a:endParaRPr lang="en-US" sz="%d"/></a:p>' % (
|
|
1145
|
+
attrs, inner, self.runs(segs, size, u["line"], color, font), size * 100)
|
|
1146
|
+
|
|
1147
|
+
def text_frame(self, paras, y, h, placeholder):
|
|
1148
|
+
starts = {}
|
|
1149
|
+
out = []
|
|
1150
|
+
for u in paras:
|
|
1151
|
+
startat = 1
|
|
1152
|
+
if u["role"] == "num":
|
|
1153
|
+
run = u.get("run")
|
|
1154
|
+
if run not in starts:
|
|
1155
|
+
starts[run] = u.get("num") or 1
|
|
1156
|
+
startat = starts[run]
|
|
1157
|
+
out.append(self.para_xml(u, startat))
|
|
1158
|
+
sid = self.sid()
|
|
1159
|
+
body = ('<p:txBody><a:bodyPr wrap="square" lIns="%d" tIns="%d" rIns="%d" bIns="%d" anchor="t">'
|
|
1160
|
+
'<a:normAutofit/></a:bodyPr><a:lstStyle/>%s</p:txBody>') % (
|
|
1161
|
+
SIDE_INSET, INSET, SIDE_INSET, INSET, "".join(out))
|
|
1162
|
+
geo = xfrm(MARGIN_X, y, self.deck.body_w, h)
|
|
1163
|
+
if placeholder:
|
|
1164
|
+
self.shapes.append(
|
|
1165
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Content %d"/><p:cNvSpPr><a:spLocks noGrp="1"/>'
|
|
1166
|
+
'</p:cNvSpPr><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr><p:spPr>%s</p:spPr>%s</p:sp>'
|
|
1167
|
+
% (sid, sid - 1, geo, body))
|
|
1168
|
+
else:
|
|
1169
|
+
self.shapes.append(
|
|
1170
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Text %d"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>'
|
|
1171
|
+
'<p:spPr>%s<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr>%s</p:sp>'
|
|
1172
|
+
% (sid, sid - 1, geo, body))
|
|
1173
|
+
|
|
1174
|
+
def code_box(self, u, y, h):
|
|
1175
|
+
sid = self.sid()
|
|
1176
|
+
paras = []
|
|
1177
|
+
for l in u["lines"]:
|
|
1178
|
+
l = l.replace("\t", " ")
|
|
1179
|
+
run = ('<a:r><a:rPr lang="en-US" sz="%d"><a:solidFill><a:srgbClr val="1F1F1F"/></a:solidFill>'
|
|
1180
|
+
'<a:latin typeface="Consolas" pitchFamily="49" charset="0"/>'
|
|
1181
|
+
'<a:cs typeface="Consolas" pitchFamily="49" charset="0"/></a:rPr><a:t>%s</a:t></a:r>'
|
|
1182
|
+
% (CODE_PT * 100, x(l))) if l else ""
|
|
1183
|
+
paras.append('<a:p><a:pPr marL="0" indent="0"><a:lnSpc><a:spcPct val="100000"/></a:lnSpc>'
|
|
1184
|
+
'<a:spcBef><a:spcPts val="0"/></a:spcBef><a:buNone/></a:pPr>%s'
|
|
1185
|
+
'<a:endParaRPr lang="en-US" sz="%d"/></a:p>' % (run, CODE_PT * 100))
|
|
1186
|
+
ins = int(0.08 * EMU_IN)
|
|
1187
|
+
self.shapes.append(
|
|
1188
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="%d" name="Code %d"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>'
|
|
1189
|
+
'<p:spPr>%s<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="F2F2F2"/>'
|
|
1190
|
+
'</a:solidFill><a:ln w="6350"><a:solidFill><a:srgbClr val="D9D9D9"/></a:solidFill></a:ln></p:spPr>'
|
|
1191
|
+
'<p:txBody><a:bodyPr wrap="square" lIns="%d" tIns="%d" rIns="%d" bIns="%d" anchor="t"><a:noAutofit/>'
|
|
1192
|
+
'</a:bodyPr><a:lstStyle/>%s</p:txBody></p:sp>'
|
|
1193
|
+
% (sid, sid - 1, xfrm(MARGIN_X, y, self.deck.body_w, h), SIDE_INSET, ins, SIDE_INSET, ins, "".join(paras)))
|
|
1194
|
+
|
|
1195
|
+
def table(self, u, y):
|
|
1196
|
+
sid = self.sid()
|
|
1197
|
+
cols, heights = u["cols"], u["heights"]
|
|
1198
|
+
width = sum(cols)
|
|
1199
|
+
xo = MARGIN_X + (self.deck.body_w - width) // 2
|
|
1200
|
+
brand = self.deck.brand
|
|
1201
|
+
head_text = brand["on_primary"]
|
|
1202
|
+
border = ('<a:%s w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:srgbClr val="BFBFBF"/>'
|
|
1203
|
+
'</a:solidFill><a:prstDash val="solid"/></a:%s>')
|
|
1204
|
+
lines = "".join(border % (e, e) for e in ("lnL", "lnR", "lnT", "lnB"))
|
|
1205
|
+
rows_xml = []
|
|
1206
|
+
for r_i, row in enumerate([u["header"]] + u["rows"]):
|
|
1207
|
+
head = r_i == 0
|
|
1208
|
+
fill = brand["primary"] if head else ("FFFFFF" if r_i % 2 else "F2F2F2")
|
|
1209
|
+
cells = []
|
|
1210
|
+
for c_i, cell in enumerate(row):
|
|
1211
|
+
segs = Inline(self.deck.refs, u["line"]).parse(cell, {"bold": True} if head else None)
|
|
1212
|
+
cells.append(
|
|
1213
|
+
'<a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr algn="%s" marL="0" indent="0"><a:buNone/>'
|
|
1214
|
+
'</a:pPr>%s<a:endParaRPr lang="en-US" sz="%d"/></a:p></a:txBody>'
|
|
1215
|
+
'<a:tcPr marL="%d" marR="%d" marT="%d" marB="%d" anchor="ctr">%s<a:solidFill>'
|
|
1216
|
+
'<a:srgbClr val="%s"/></a:solidFill></a:tcPr></a:tc>' % (
|
|
1217
|
+
u["aligns"][c_i], self.runs(segs, TABLE_PT, u["line"], head_text if head else None),
|
|
1218
|
+
TABLE_PT * 100, CELL_MAR_X, CELL_MAR_X, CELL_MAR_Y, CELL_MAR_Y, lines, fill))
|
|
1219
|
+
rows_xml.append('<a:tr h="%d">%s</a:tr>' % (heights[r_i], "".join(cells)))
|
|
1220
|
+
grid = "".join('<a:gridCol w="%d"/>' % c for c in cols)
|
|
1221
|
+
self.shapes.append(
|
|
1222
|
+
'<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="%d" name="Table %d"/><p:cNvGraphicFramePr>'
|
|
1223
|
+
'<a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>'
|
|
1224
|
+
'<p:xfrm><a:off x="%d" y="%d"/><a:ext cx="%d" cy="%d"/></p:xfrm><a:graphic>'
|
|
1225
|
+
'<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"><a:tbl>'
|
|
1226
|
+
'<a:tblPr firstRow="1" bandRow="1"><a:tableStyleId>{2D5ABB26-0587-4C30-8999-92F81FD0307C}'
|
|
1227
|
+
'</a:tableStyleId></a:tblPr><a:tblGrid>%s</a:tblGrid>%s</a:tbl></a:graphicData></a:graphic>'
|
|
1228
|
+
'</p:graphicFrame>' % (sid, sid - 1, xo, y, width, sum(heights), grid, "".join(rows_xml)))
|
|
1229
|
+
|
|
1230
|
+
def picture(self, u, y):
|
|
1231
|
+
deck = self.deck
|
|
1232
|
+
name = deck.media_by_path.get(u["path"])
|
|
1233
|
+
if not name:
|
|
1234
|
+
name = "image%d.%s" % (len(deck.media) + 1, u["fmt"])
|
|
1235
|
+
with open(u["path"], "rb") as f:
|
|
1236
|
+
deck.media.append(("ppt/media/" + name, f.read()))
|
|
1237
|
+
deck.media_by_path[u["path"]] = name
|
|
1238
|
+
deck.counts["images"] += 1
|
|
1239
|
+
rid = self.add_rel("image", "../media/" + name)
|
|
1240
|
+
sid = self.sid()
|
|
1241
|
+
xo = MARGIN_X + (deck.body_w - u["cx"]) // 2
|
|
1242
|
+
self.shapes.append(
|
|
1243
|
+
'<p:pic><p:nvPicPr><p:cNvPr id="%d" name="Picture %d" descr="%s"/><p:cNvPicPr>'
|
|
1244
|
+
'<a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill>'
|
|
1245
|
+
'<a:blip r:embed="%s"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr>%s'
|
|
1246
|
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>'
|
|
1247
|
+
% (sid, sid - 1, x(u["alt"]), rid, xfrm(xo, y, u["cx"], u["cy"])))
|
|
1248
|
+
|
|
1249
|
+
def layout_body(self, units):
|
|
1250
|
+
y = BODY_Y
|
|
1251
|
+
placeholder_used = False
|
|
1252
|
+
i = 0
|
|
1253
|
+
while i < len(units):
|
|
1254
|
+
u = units[i]
|
|
1255
|
+
k = u["kind"]
|
|
1256
|
+
if k == "notes":
|
|
1257
|
+
self.notes.append(u["text"])
|
|
1258
|
+
i += 1
|
|
1259
|
+
continue
|
|
1260
|
+
if y > BODY_Y:
|
|
1261
|
+
y += GAP
|
|
1262
|
+
if k == "para":
|
|
1263
|
+
group = []
|
|
1264
|
+
while i < len(units) and units[i]["kind"] in ("para", "notes"):
|
|
1265
|
+
if units[i]["kind"] == "notes":
|
|
1266
|
+
self.notes.append(units[i]["text"])
|
|
1267
|
+
else:
|
|
1268
|
+
group.append(units[i])
|
|
1269
|
+
i += 1
|
|
1270
|
+
h = sum(p["h"] for p in group) + 2 * INSET
|
|
1271
|
+
self.text_frame(group, y, h, not placeholder_used)
|
|
1272
|
+
placeholder_used = True
|
|
1273
|
+
y += h
|
|
1274
|
+
continue
|
|
1275
|
+
if k == "code":
|
|
1276
|
+
h = self.deck.code_lines_h(u["lines"])
|
|
1277
|
+
self.code_box(u, y, h)
|
|
1278
|
+
y += h
|
|
1279
|
+
elif k == "table":
|
|
1280
|
+
self.table(u, y)
|
|
1281
|
+
y += sum(u["heights"])
|
|
1282
|
+
elif k == "image":
|
|
1283
|
+
self.picture(u, y)
|
|
1284
|
+
y += u["cy"]
|
|
1285
|
+
i += 1
|
|
1286
|
+
|
|
1287
|
+
def xml(self):
|
|
1288
|
+
return (XML_HEAD + '<p:sld %s><p:cSld><p:spTree>%s%s</p:spTree></p:cSld>'
|
|
1289
|
+
'<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>') % (NS, GRP, "".join(self.shapes))
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def notes_xml(texts, width):
|
|
1293
|
+
paras = []
|
|
1294
|
+
for t in texts:
|
|
1295
|
+
for line in t.split("\n"):
|
|
1296
|
+
paras.append('<a:p><a:r><a:rPr lang="en-US"/><a:t>%s</a:t></a:r></a:p>' % x(line) if line.strip()
|
|
1297
|
+
else '<a:p><a:endParaRPr lang="en-US"/></a:p>')
|
|
1298
|
+
img_h = int(5486400 * SLIDE_H / float(width))
|
|
1299
|
+
return (XML_HEAD + '<p:notes %s><p:cSld><p:spTree>%s'
|
|
1300
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" '
|
|
1301
|
+
'noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr>'
|
|
1302
|
+
'<p:spPr>%s</p:spPr></p:sp>'
|
|
1303
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/>'
|
|
1304
|
+
'</p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr>%s</p:spPr>'
|
|
1305
|
+
'<p:txBody><a:bodyPr/><a:lstStyle/>%s</p:txBody></p:sp>'
|
|
1306
|
+
'</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>') % (
|
|
1307
|
+
NS, GRP, xfrm(685800, 1143000, 5486400, img_h), xfrm(685800, 4400550, 5486400, 3600450),
|
|
1308
|
+
"".join(paras))
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
# ---- fixed parts --------------------------------------------------------------------
|
|
1312
|
+
|
|
1313
|
+
def theme_xml(brand, name):
|
|
1314
|
+
accents = [brand["primary"], brand["accent"], "A5A5A5", "FFC000", "5B9BD5", "70AD47"]
|
|
1315
|
+
clr = ('<a:dk1><a:srgbClr val="%s"/></a:dk1><a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>'
|
|
1316
|
+
'<a:dk2><a:srgbClr val="%s"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>' % (brand["text"], brand["primary"]))
|
|
1317
|
+
clr += "".join('<a:accent%d><a:srgbClr val="%s"/></a:accent%d>' % (i + 1, c, i + 1) for i, c in enumerate(accents))
|
|
1318
|
+
clr += '<a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink>'
|
|
1319
|
+
ln = ('<a:ln w="%d" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
|
|
1320
|
+
'<a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>')
|
|
1321
|
+
solid = '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
|
|
1322
|
+
return (XML_HEAD + '<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="%s">'
|
|
1323
|
+
'<a:themeElements><a:clrScheme name="%s">%s</a:clrScheme>'
|
|
1324
|
+
'<a:fontScheme name="%s"><a:majorFont><a:latin typeface="%s"/><a:ea typeface=""/><a:cs typeface=""/>'
|
|
1325
|
+
'</a:majorFont><a:minorFont><a:latin typeface="%s"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>'
|
|
1326
|
+
'</a:fontScheme><a:fmtScheme name="%s"><a:fillStyleLst>%s</a:fillStyleLst><a:lnStyleLst>%s</a:lnStyleLst>'
|
|
1327
|
+
'<a:effectStyleLst>%s</a:effectStyleLst><a:bgFillStyleLst>%s</a:bgFillStyleLst></a:fmtScheme>'
|
|
1328
|
+
'</a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>') % (
|
|
1329
|
+
x(name), x(name), clr, x(name), x(brand["heading_font"]), x(brand["body_font"]), x(name),
|
|
1330
|
+
solid * 3, ln % 6350 + ln % 12700 + ln % 19050,
|
|
1331
|
+
'<a:effectStyle><a:effectLst/></a:effectStyle>' * 3, solid * 3)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def text_styles():
|
|
1335
|
+
def lvl(n, marl, ind, sz, char, spc):
|
|
1336
|
+
return ('<a:lvl%dpPr marL="%d" indent="%d" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" '
|
|
1337
|
+
'hangingPunct="1"><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:spcBef><a:spcPts val="%d"/>'
|
|
1338
|
+
'</a:spcBef><a:buFont typeface="Arial"/><a:buChar char="%s"/><a:defRPr sz="%d" kern="1200">'
|
|
1339
|
+
'<a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/>'
|
|
1340
|
+
'<a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl%dpPr>') % (
|
|
1341
|
+
n, marl, ind, spc, char, sz, n)
|
|
1342
|
+
body = "".join(lvl(i + 1, int(SIZES[i][1] * EMU_IN), -int(SIZES[i][2] * EMU_IN), SIZES[i][0] * 100,
|
|
1343
|
+
BULLETS[i], SIZES[i][3] * 100) for i in range(3))
|
|
1344
|
+
title = ('<p:titleStyle><a:lvl1pPr algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" '
|
|
1345
|
+
'hangingPunct="1"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc><a:spcBef><a:spcPct val="0"/></a:spcBef>'
|
|
1346
|
+
'<a:buNone/><a:defRPr sz="%d" b="1" kern="1200"><a:solidFill><a:schemeClr val="tx2"/></a:solidFill>'
|
|
1347
|
+
'<a:latin typeface="+mj-lt"/><a:ea typeface="+mj-ea"/><a:cs typeface="+mj-cs"/></a:defRPr>'
|
|
1348
|
+
'</a:lvl1pPr></p:titleStyle>') % (TITLE_PT * 100)
|
|
1349
|
+
other = ('<p:otherStyle><a:defPPr><a:defRPr lang="en-US"/></a:defPPr><a:lvl1pPr marL="0" algn="l" '
|
|
1350
|
+
'defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200">'
|
|
1351
|
+
'<a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/>'
|
|
1352
|
+
'<a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr></p:otherStyle>')
|
|
1353
|
+
return '<p:txStyles>%s<p:bodyStyle>%s</p:bodyStyle>%s</p:txStyles>' % (title, body, other)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def ph_sp(sid, name, ph, geo, body_pr, lst, text):
|
|
1357
|
+
return ('<p:sp><p:nvSpPr><p:cNvPr id="%d" name="%s"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>'
|
|
1358
|
+
'<p:nvPr>%s</p:nvPr></p:nvSpPr><p:spPr>%s<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>'
|
|
1359
|
+
'<p:txBody>%s<a:lstStyle>%s</a:lstStyle><a:p><a:r><a:rPr lang="en-US"/><a:t>%s</a:t></a:r></a:p>'
|
|
1360
|
+
'</p:txBody></p:sp>') % (sid, name, ph, geo, body_pr, lst, text)
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
CLRMAP = ('bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" '
|
|
1364
|
+
'accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"')
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def master_xml(width):
|
|
1368
|
+
bw = width - 2 * MARGIN_X
|
|
1369
|
+
title = ph_sp(2, "Title Placeholder 1", '<p:ph type="title"/>', xfrm(MARGIN_X, TITLE_Y, bw, TITLE_H),
|
|
1370
|
+
'<a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b">'
|
|
1371
|
+
'<a:normAutofit/></a:bodyPr>', "", "Click to edit Master title style")
|
|
1372
|
+
body = ph_sp(3, "Text Placeholder 2", '<p:ph type="body" idx="1"/>',
|
|
1373
|
+
xfrm(MARGIN_X, BODY_Y, bw, BODY_BOTTOM - BODY_Y),
|
|
1374
|
+
'<a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0">'
|
|
1375
|
+
'<a:normAutofit/></a:bodyPr>', "", "Click to edit Master text styles")
|
|
1376
|
+
rule = ('<p:cxnSp><p:nvCxnSpPr><p:cNvPr id="4" name="Title Rule"/><p:cNvCxnSpPr/><p:nvPr userDrawn="1"/>'
|
|
1377
|
+
'</p:nvCxnSpPr><p:spPr>%s<a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:ln w="19050">'
|
|
1378
|
+
'<a:solidFill><a:schemeClr val="accent2"/></a:solidFill></a:ln></p:spPr></p:cxnSp>') % xfrm(
|
|
1379
|
+
MARGIN_X + SIDE_INSET, TITLE_Y + TITLE_H + 45720, int(1.2 * EMU_IN), 0)
|
|
1380
|
+
return (XML_HEAD + '<p:sldMaster %s><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef>'
|
|
1381
|
+
'</p:bg><p:spTree>%s%s%s%s</p:spTree></p:cSld><p:clrMap %s/><p:sldLayoutIdLst>'
|
|
1382
|
+
'<p:sldLayoutId id="2147483649" r:id="rId1"/><p:sldLayoutId id="2147483650" r:id="rId2"/>'
|
|
1383
|
+
'</p:sldLayoutIdLst>%s</p:sldMaster>') % (NS, GRP, title, body, rule, CLRMAP, text_styles())
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def layout_xml(kind, width):
|
|
1387
|
+
bw = width - 2 * MARGIN_X
|
|
1388
|
+
if kind == 1:
|
|
1389
|
+
t = ph_sp(2, "Title 1", '<p:ph type="ctrTitle"/>', xfrm(MARGIN_X, int(1.9 * EMU_IN), bw, int(1.9 * EMU_IN)),
|
|
1390
|
+
'<a:bodyPr anchor="b"><a:normAutofit/></a:bodyPr>',
|
|
1391
|
+
'<a:lvl1pPr algn="ctr"><a:defRPr sz="4000"/></a:lvl1pPr>', "Click to edit Master title style")
|
|
1392
|
+
s = ph_sp(3, "Subtitle 2", '<p:ph type="subTitle" idx="1"/>',
|
|
1393
|
+
xfrm(MARGIN_X, int(4.0 * EMU_IN), bw, int(1.6 * EMU_IN)),
|
|
1394
|
+
'<a:bodyPr><a:normAutofit/></a:bodyPr>',
|
|
1395
|
+
'<a:lvl1pPr marL="0" indent="0" algn="ctr"><a:spcBef><a:spcPts val="600"/></a:spcBef><a:buNone/>'
|
|
1396
|
+
'<a:defRPr sz="2000"><a:solidFill><a:srgbClr val="595959"/></a:solidFill></a:defRPr></a:lvl1pPr>',
|
|
1397
|
+
"Click to edit Master subtitle style")
|
|
1398
|
+
return (XML_HEAD + '<p:sldLayout %s type="title" preserve="1" showMasterSp="0"><p:cSld name="Title Slide"><p:spTree>%s%s%s'
|
|
1399
|
+
'</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>') % (NS, GRP, t, s)
|
|
1400
|
+
t = ph_sp(2, "Title 1", '<p:ph type="title"/>', xfrm(MARGIN_X, TITLE_Y, bw, TITLE_H),
|
|
1401
|
+
'<a:bodyPr/>', "", "Click to edit Master title style")
|
|
1402
|
+
b = ph_sp(3, "Content Placeholder 2", '<p:ph idx="1"/>', xfrm(MARGIN_X, BODY_Y, bw, BODY_BOTTOM - BODY_Y),
|
|
1403
|
+
'<a:bodyPr/>', "", "Click to edit Master text styles")
|
|
1404
|
+
return (XML_HEAD + '<p:sldLayout %s type="obj" preserve="1"><p:cSld name="Title and Content"><p:spTree>%s%s%s'
|
|
1405
|
+
'</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>') % (NS, GRP, t, b)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def notes_master_xml(width):
|
|
1409
|
+
img_h = int(5486400 * SLIDE_H / float(width))
|
|
1410
|
+
return (XML_HEAD + '<p:notesMaster %s><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef>'
|
|
1411
|
+
'</p:bg><p:spTree>%s'
|
|
1412
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" '
|
|
1413
|
+
'noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr></p:nvSpPr>'
|
|
1414
|
+
'<p:spPr>%s<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln w="12700"><a:solidFill>'
|
|
1415
|
+
'<a:prstClr val="black"/></a:solidFill></a:ln></p:spPr></p:sp>'
|
|
1416
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/>'
|
|
1417
|
+
'</p:cNvSpPr><p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr></p:nvSpPr><p:spPr>%s'
|
|
1418
|
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" '
|
|
1419
|
+
'tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/><a:lstStyle/><a:p><a:pPr lvl="0"/><a:r>'
|
|
1420
|
+
'<a:rPr lang="en-US"/><a:t>Click to edit Master text styles</a:t></a:r></a:p></p:txBody></p:sp>'
|
|
1421
|
+
'</p:spTree></p:cSld><p:clrMap %s/><p:notesStyle><a:lvl1pPr marL="0" algn="l" defTabSz="914400" '
|
|
1422
|
+
'rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill>'
|
|
1423
|
+
'<a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/>'
|
|
1424
|
+
'<a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr></p:notesStyle></p:notesMaster>') % (
|
|
1425
|
+
NS, GRP, xfrm(685800, 1143000, 5486400, img_h), xfrm(685800, 4400550, 5486400, 3600450), CLRMAP)
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
# =========================================================================== brand
|
|
1429
|
+
|
|
1430
|
+
def hex6(val):
|
|
1431
|
+
s = str(val).strip()
|
|
1432
|
+
if re.match(r"^#?[0-9A-Fa-f]{6}$", s):
|
|
1433
|
+
return s.lstrip("#").upper()
|
|
1434
|
+
return None
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def luminance(h):
|
|
1438
|
+
def ch(v):
|
|
1439
|
+
v = v / 255.0
|
|
1440
|
+
return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4
|
|
1441
|
+
r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
1442
|
+
return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b)
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def contrast(a, b):
|
|
1446
|
+
la, lb = sorted((luminance(a), luminance(b)), reverse=True)
|
|
1447
|
+
return (la + 0.05) / (lb + 0.05)
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
def load_brand(path):
|
|
1451
|
+
brand = {"heading_font": "Calibri", "body_font": "Calibri", "primary": "1F3864", "accent": "2E75B6",
|
|
1452
|
+
"text": "222222"}
|
|
1453
|
+
branded = False
|
|
1454
|
+
if path:
|
|
1455
|
+
try:
|
|
1456
|
+
with open(path, encoding="utf-8") as f:
|
|
1457
|
+
data = json.load(f)
|
|
1458
|
+
except (OSError, ValueError) as e:
|
|
1459
|
+
die("cannot read brand file %s: %s" % (path, e))
|
|
1460
|
+
if not isinstance(data, dict):
|
|
1461
|
+
die("brand file %s is not a JSON object" % path)
|
|
1462
|
+
branded = True
|
|
1463
|
+
fonts = data.get("fonts") if isinstance(data.get("fonts"), dict) else {}
|
|
1464
|
+
colors = data.get("colors") if isinstance(data.get("colors"), dict) else {}
|
|
1465
|
+
for key, field in (("heading_font", "heading"), ("body_font", "body")):
|
|
1466
|
+
val = fonts.get(field)
|
|
1467
|
+
fam = val.get("family") if isinstance(val, dict) else val if isinstance(val, str) else None
|
|
1468
|
+
if fam and str(fam).strip():
|
|
1469
|
+
brand[key] = str(fam).strip()
|
|
1470
|
+
for key in ("primary", "accent", "text"):
|
|
1471
|
+
val = colors.get(key)
|
|
1472
|
+
if val:
|
|
1473
|
+
h = hex6(val)
|
|
1474
|
+
if h:
|
|
1475
|
+
brand[key] = h
|
|
1476
|
+
else:
|
|
1477
|
+
warn(0, "brand colors.%s %r is not a 6-digit hex code; default #%s kept" % (key, val, brand[key]))
|
|
1478
|
+
if contrast(brand["primary"], "FFFFFF") < 3.0:
|
|
1479
|
+
warn(0, "brand primary #%s is too light for slide titles on white (contrast %.1f:1); "
|
|
1480
|
+
"titles use the text colour instead" % (brand["primary"], contrast(brand["primary"], "FFFFFF")))
|
|
1481
|
+
brand["primary_for_text"] = brand["text"]
|
|
1482
|
+
brand["on_primary"] = "FFFFFF" if contrast(brand["primary"], "FFFFFF") >= contrast(brand["primary"], "1F1F1F") \
|
|
1483
|
+
else "1F1F1F"
|
|
1484
|
+
return brand, branded
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
# =========================================================================== verification
|
|
1488
|
+
|
|
1489
|
+
def verify(path):
|
|
1490
|
+
"""Re-open the package: XML parses, internal targets exist, every part has a content type."""
|
|
1491
|
+
problems = []
|
|
1492
|
+
with zipfile.ZipFile(path) as z:
|
|
1493
|
+
names = set(z.namelist())
|
|
1494
|
+
xml_parts = [n for n in names if n.endswith(".xml") or n.endswith(".rels")]
|
|
1495
|
+
for n in xml_parts:
|
|
1496
|
+
try:
|
|
1497
|
+
minidom.parseString(z.read(n))
|
|
1498
|
+
except Exception as e: # noqa: BLE001 - report any parser failure
|
|
1499
|
+
problems.append("%s does not parse: %s" % (n, e))
|
|
1500
|
+
ct = minidom.parseString(z.read("[Content_Types].xml"))
|
|
1501
|
+
defaults = {e.getAttribute("Extension").lower() for e in ct.getElementsByTagName("Default")}
|
|
1502
|
+
overrides = {e.getAttribute("PartName") for e in ct.getElementsByTagName("Override")}
|
|
1503
|
+
for o in overrides:
|
|
1504
|
+
if o.lstrip("/") not in names:
|
|
1505
|
+
problems.append("content type declared for missing part %s" % o)
|
|
1506
|
+
for n in names:
|
|
1507
|
+
if n == "[Content_Types].xml" or n.endswith("/"):
|
|
1508
|
+
continue
|
|
1509
|
+
if "/" + n not in overrides and n.rsplit(".", 1)[-1].lower() not in defaults:
|
|
1510
|
+
problems.append("no content type for %s" % n)
|
|
1511
|
+
n_rels = 0
|
|
1512
|
+
for n in names:
|
|
1513
|
+
if not n.endswith(".rels"):
|
|
1514
|
+
continue
|
|
1515
|
+
folder = posixpath.dirname(posixpath.dirname(n))
|
|
1516
|
+
ids = set()
|
|
1517
|
+
for r in minidom.parseString(z.read(n)).getElementsByTagName("Relationship"):
|
|
1518
|
+
n_rels += 1
|
|
1519
|
+
rid = r.getAttribute("Id")
|
|
1520
|
+
if rid in ids:
|
|
1521
|
+
problems.append("%s repeats relationship id %s" % (n, rid))
|
|
1522
|
+
ids.add(rid)
|
|
1523
|
+
if r.getAttribute("TargetMode") == "External":
|
|
1524
|
+
continue
|
|
1525
|
+
t = r.getAttribute("Target")
|
|
1526
|
+
full = posixpath.normpath(t.lstrip("/") if t.startswith("/") else posixpath.join(folder, t))
|
|
1527
|
+
if full not in names:
|
|
1528
|
+
problems.append("%s: %s points to missing %s" % (n, rid, full))
|
|
1529
|
+
src = n.replace("_rels/", "").rsplit(".rels", 1)[0]
|
|
1530
|
+
if src in names and src.endswith(".xml"):
|
|
1531
|
+
used = set(re.findall(r'r:(?:id|embed)="([^"]+)"', z.read(src).decode("utf-8")))
|
|
1532
|
+
for rid in used - ids:
|
|
1533
|
+
problems.append("%s uses %s, which %s does not define" % (src, rid, n))
|
|
1534
|
+
if problems:
|
|
1535
|
+
for p in problems:
|
|
1536
|
+
print("ERROR verify: %s" % p, file=sys.stderr)
|
|
1537
|
+
die("the written file failed its own check; please report this with the input", code=2)
|
|
1538
|
+
return len(xml_parts), n_rels
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
# =========================================================================== main
|
|
1542
|
+
|
|
1543
|
+
def front_matter(lines):
|
|
1544
|
+
fm, base = {}, 1
|
|
1545
|
+
if lines and lines[0].strip() == "---":
|
|
1546
|
+
for k in range(1, min(len(lines), 200)):
|
|
1547
|
+
if lines[k].strip() in ("---", "..."):
|
|
1548
|
+
for fl in lines[1:k]:
|
|
1549
|
+
m = re.match(r"^(title|subtitle|author|date|presenter)\s*:\s*[\"']?(.*?)[\"']?\s*$", fl, re.I)
|
|
1550
|
+
if m and m.group(2):
|
|
1551
|
+
fm[m.group(1).lower()] = m.group(2)
|
|
1552
|
+
used = ", ".join(k2 for k2 in ("title", "subtitle", "author", "presenter", "date") if k2 in fm)
|
|
1553
|
+
warn(1, "YAML front matter dropped (lines 1-%d)%s" % (
|
|
1554
|
+
k + 1, "; used on the title slide: %s" % used if used else ""))
|
|
1555
|
+
return fm, lines[k + 1:], k + 2
|
|
1556
|
+
return fm, lines, base
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def convert(src, out, brand_path, title_arg, aspect):
|
|
1560
|
+
if src == "-":
|
|
1561
|
+
text = sys.stdin.read()
|
|
1562
|
+
src_dir = os.getcwd()
|
|
1563
|
+
stem = "slides"
|
|
1564
|
+
else:
|
|
1565
|
+
try:
|
|
1566
|
+
with open(src, encoding="utf-8-sig") as f:
|
|
1567
|
+
text = f.read()
|
|
1568
|
+
except OSError as e:
|
|
1569
|
+
die("cannot read %s: %s" % (src, e))
|
|
1570
|
+
except UnicodeDecodeError:
|
|
1571
|
+
die("%s is not UTF-8 text; save it as UTF-8 and rerun" % src)
|
|
1572
|
+
src_dir = os.path.dirname(os.path.abspath(src))
|
|
1573
|
+
stem = os.path.splitext(os.path.basename(src))[0]
|
|
1574
|
+
if "\x00" in text:
|
|
1575
|
+
die("%s looks like a binary file, not Markdown" % src)
|
|
1576
|
+
if not text.strip():
|
|
1577
|
+
die("%s is empty" % src)
|
|
1578
|
+
out_dir = os.path.dirname(os.path.abspath(out))
|
|
1579
|
+
if not os.path.isdir(out_dir):
|
|
1580
|
+
die("output folder %s does not exist" % out_dir)
|
|
1581
|
+
|
|
1582
|
+
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
1583
|
+
fm, lines, base = front_matter(lines)
|
|
1584
|
+
brand, branded = load_brand(brand_path)
|
|
1585
|
+
refs = {}
|
|
1586
|
+
blocks = parse_blocks(lines, base, refs)
|
|
1587
|
+
deck = Deck(src_dir, aspect, brand, refs)
|
|
1588
|
+
|
|
1589
|
+
# ---- title, subtitle, and the sections that become slides
|
|
1590
|
+
# the title slide shows what a reader of the Markdown sees: --title, else the first # heading
|
|
1591
|
+
# (when it is the first heading), else the front matter title
|
|
1592
|
+
title, title_line = title_arg, 0
|
|
1593
|
+
first_heading = next((b for b in blocks if b["type"] == "heading"), None)
|
|
1594
|
+
if first_heading is not None and first_heading["level"] == 1:
|
|
1595
|
+
if title:
|
|
1596
|
+
warn(first_heading["line"], "first # heading \"%s\" replaced on the title slide by --title"
|
|
1597
|
+
% first_heading["text"][:50])
|
|
1598
|
+
else:
|
|
1599
|
+
title, title_line = first_heading["text"], first_heading["line"]
|
|
1600
|
+
blocks.remove(first_heading)
|
|
1601
|
+
if not title:
|
|
1602
|
+
title = fm.get("title")
|
|
1603
|
+
pre, sections, cur = [], [], None
|
|
1604
|
+
for b in blocks:
|
|
1605
|
+
if b["type"] == "heading" and b["level"] <= 2:
|
|
1606
|
+
cur = {"title": b["text"], "line": b["line"], "level": b["level"], "blocks": []}
|
|
1607
|
+
sections.append(cur)
|
|
1608
|
+
elif cur is None:
|
|
1609
|
+
pre.append(b)
|
|
1610
|
+
else:
|
|
1611
|
+
cur["blocks"].append(b)
|
|
1612
|
+
|
|
1613
|
+
sub_lines = []
|
|
1614
|
+
for key in ("subtitle", "author", "presenter", "date"):
|
|
1615
|
+
if fm.get(key):
|
|
1616
|
+
sub_lines.append(fm[key])
|
|
1617
|
+
title_notes = [b["text"] for b in pre if b["type"] == "notes"]
|
|
1618
|
+
content_pre = [b for b in pre if b["type"] != "notes"]
|
|
1619
|
+
if (title and not sub_lines and len(content_pre) == 1 and content_pre[0]["type"] == "para"
|
|
1620
|
+
and len(" ".join(l for _, l in content_pre[0]["lines"])) <= 200):
|
|
1621
|
+
sub_lines.append("\n".join(l for _, l in content_pre[0]["lines"]).strip())
|
|
1622
|
+
content_pre = []
|
|
1623
|
+
if title:
|
|
1624
|
+
if content_pre:
|
|
1625
|
+
sections.insert(0, {"title": title, "line": title_line, "level": 2,
|
|
1626
|
+
"blocks": content_pre})
|
|
1627
|
+
else:
|
|
1628
|
+
if content_pre:
|
|
1629
|
+
sections.insert(0, {"title": stem, "line": 0, "level": 2, "blocks": content_pre})
|
|
1630
|
+
warn(0, "text before the first ## heading has no heading; its slide is titled with the file name "
|
|
1631
|
+
"\"%s\"" % stem)
|
|
1632
|
+
warn(0, "no # title, front matter title or --title; the deck has no title slide")
|
|
1633
|
+
title_notes = []
|
|
1634
|
+
if not sections and not title:
|
|
1635
|
+
die("%s has nothing to put on slides" % src)
|
|
1636
|
+
|
|
1637
|
+
# ---- paginate every section
|
|
1638
|
+
plan = [] # dicts: kind, title_segs, units, layout, section, page, pages
|
|
1639
|
+
if title:
|
|
1640
|
+
plan.append({"kind": "title", "title": title, "line": title_line, "units": [], "sub": sub_lines,
|
|
1641
|
+
"notes": title_notes})
|
|
1642
|
+
anchors = {}
|
|
1643
|
+
splits = []
|
|
1644
|
+
for s in sections:
|
|
1645
|
+
units = deck.units_from_blocks(s["blocks"])
|
|
1646
|
+
if s["level"] == 1 and not [u for u in units if u["kind"] not in ("notes", "break")]:
|
|
1647
|
+
anchors.setdefault(slugify(s["title"]), len(plan) + 1)
|
|
1648
|
+
plan.append({"kind": "section", "title": s["title"], "line": s["line"], "units": [], "sub": [],
|
|
1649
|
+
"notes": [u["text"] for u in units if u["kind"] == "notes"]})
|
|
1650
|
+
continue
|
|
1651
|
+
title_lines = text_lines(s["title"], TITLE_PT, deck.body_w - 2 * SIDE_INSET, deck.head_factor * 1.05)
|
|
1652
|
+
if title_lines > 2:
|
|
1653
|
+
warn(s["line"], "slide title \"%s\" wraps to %d lines; shorten it to fit two" % (
|
|
1654
|
+
s["title"][:50], title_lines))
|
|
1655
|
+
pages, reasons = deck.paginate(units, s["title"], s["line"])
|
|
1656
|
+
first = len(plan) + 1
|
|
1657
|
+
anchors.setdefault(slugify(s["title"]), first)
|
|
1658
|
+
for p_i, page in enumerate(pages):
|
|
1659
|
+
for u in page:
|
|
1660
|
+
if u.get("slug"):
|
|
1661
|
+
anchors.setdefault(u["slug"], len(plan) + 1)
|
|
1662
|
+
plan.append({"kind": "content", "title": s["title"] + (" (continued)" if p_i else ""),
|
|
1663
|
+
"line": s["line"], "units": page, "sub": [], "notes": []})
|
|
1664
|
+
if reasons:
|
|
1665
|
+
splits.append((s, first, first + len(pages) - 1, sorted(set(reasons))))
|
|
1666
|
+
|
|
1667
|
+
for s, a, b, why in splits:
|
|
1668
|
+
warn(s["line"], "\"%s\" did not fit on one slide; split into %d slides (%d-%d) at a %s boundary, "
|
|
1669
|
+
"text kept at full size" % (s["title"][:60], b - a + 1, a, b, " / ".join(why)))
|
|
1670
|
+
|
|
1671
|
+
# ---- render
|
|
1672
|
+
width = deck.width
|
|
1673
|
+
slides = []
|
|
1674
|
+
for n, item in enumerate(plan, 1):
|
|
1675
|
+
layout = 2 if item["kind"] == "content" else 1
|
|
1676
|
+
so = SlideOut(deck, layout, anchors, n)
|
|
1677
|
+
segs = deck.inline(item["title"], item["line"])
|
|
1678
|
+
segs = [s for s in segs if s[0] != "img"]
|
|
1679
|
+
if item["kind"] == "content":
|
|
1680
|
+
so.title(segs, item["line"])
|
|
1681
|
+
so.layout_body(item["units"])
|
|
1682
|
+
else:
|
|
1683
|
+
so.title(segs, item["line"], ctr=True,
|
|
1684
|
+
sub_lines=[deck.inline(t, item["line"]) for t in item["sub"]])
|
|
1685
|
+
so.notes = item["notes"] + so.notes
|
|
1686
|
+
slides.append(so)
|
|
1687
|
+
|
|
1688
|
+
# ---- package
|
|
1689
|
+
n_notes = sum(1 for s in slides if s.notes)
|
|
1690
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1691
|
+
pres_rels = [("rId1", "slideMaster", "slideMasters/slideMaster1.xml", False)]
|
|
1692
|
+
sld_ids = []
|
|
1693
|
+
for i in range(len(slides)):
|
|
1694
|
+
rid = "rId%d" % (len(pres_rels) + 1)
|
|
1695
|
+
pres_rels.append((rid, "slide", "slides/slide%d.xml" % (i + 1), False))
|
|
1696
|
+
sld_ids.append('<p:sldId id="%d" r:id="%s"/>' % (256 + i, rid))
|
|
1697
|
+
notes_master_id = ""
|
|
1698
|
+
if n_notes:
|
|
1699
|
+
rid = "rId%d" % (len(pres_rels) + 1)
|
|
1700
|
+
pres_rels.append((rid, "notesMaster", "notesMasters/notesMaster1.xml", False))
|
|
1701
|
+
notes_master_id = '<p:notesMasterIdLst><p:notesMasterId r:id="%s"/></p:notesMasterIdLst>' % rid
|
|
1702
|
+
for kind, target in (("theme", "theme/theme1.xml"), ("presProps", "presProps.xml"),
|
|
1703
|
+
("viewProps", "viewProps.xml"), ("tableStyles", "tableStyles.xml")):
|
|
1704
|
+
pres_rels.append(("rId%d" % (len(pres_rels) + 1), kind, target, False))
|
|
1705
|
+
presentation = (XML_HEAD + '<p:presentation %s saveSubsetFonts="1"><p:sldMasterIdLst>'
|
|
1706
|
+
'<p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>%s<p:sldIdLst>%s</p:sldIdLst>'
|
|
1707
|
+
'<p:sldSz cx="%d" cy="%d"%s/><p:notesSz cx="6858000" cy="9144000"/>'
|
|
1708
|
+
'<p:defaultTextStyle><a:defPPr><a:defRPr lang="en-US"/></a:defPPr></p:defaultTextStyle>'
|
|
1709
|
+
'</p:presentation>') % (NS, notes_master_id, "".join(sld_ids), width, SLIDE_H,
|
|
1710
|
+
' type="screen4x3"' if aspect == "4:3" else "")
|
|
1711
|
+
overrides = [("/ppt/presentation.xml", CT + "presentation.main+xml"),
|
|
1712
|
+
("/ppt/slideMasters/slideMaster1.xml", CT + "slideMaster+xml"),
|
|
1713
|
+
("/ppt/slideLayouts/slideLayout1.xml", CT + "slideLayout+xml"),
|
|
1714
|
+
("/ppt/slideLayouts/slideLayout2.xml", CT + "slideLayout+xml"),
|
|
1715
|
+
("/ppt/theme/theme1.xml", "application/vnd.openxmlformats-officedocument.theme+xml"),
|
|
1716
|
+
("/ppt/presProps.xml", CT + "presProps+xml"),
|
|
1717
|
+
("/ppt/viewProps.xml", CT + "viewProps+xml"),
|
|
1718
|
+
("/ppt/tableStyles.xml", CT + "tableStyles+xml"),
|
|
1719
|
+
("/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml"),
|
|
1720
|
+
("/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml")]
|
|
1721
|
+
if n_notes:
|
|
1722
|
+
overrides.append(("/ppt/notesMasters/notesMaster1.xml", CT + "notesMaster+xml"))
|
|
1723
|
+
overrides.append(("/ppt/theme/theme2.xml", "application/vnd.openxmlformats-officedocument.theme+xml"))
|
|
1724
|
+
for i, s in enumerate(slides, 1):
|
|
1725
|
+
overrides.append(("/ppt/slides/slide%d.xml" % i, CT + "slide+xml"))
|
|
1726
|
+
if s.notes:
|
|
1727
|
+
overrides.append(("/ppt/notesSlides/notesSlide%d.xml" % i, CT + "notesSlide+xml"))
|
|
1728
|
+
content_types = (XML_HEAD + '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
1729
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
1730
|
+
'<Default Extension="xml" ContentType="application/xml"/>'
|
|
1731
|
+
'<Default Extension="png" ContentType="image/png"/>'
|
|
1732
|
+
'<Default Extension="jpeg" ContentType="image/jpeg"/>%s</Types>') % "".join(
|
|
1733
|
+
'<Override PartName="%s" ContentType="%s"/>' % o for o in overrides)
|
|
1734
|
+
root_rels = rels_xml([("rId1", "officeDocument", "ppt/presentation.xml", False),
|
|
1735
|
+
("rId2", "metadata/core-properties", "docProps/core.xml", False),
|
|
1736
|
+
("rId3", "extended-properties", "docProps/app.xml", False)])
|
|
1737
|
+
root_rels = root_rels.replace(REL + "metadata/core-properties",
|
|
1738
|
+
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties")
|
|
1739
|
+
doc_title = fm.get("title") or title or stem
|
|
1740
|
+
core = (XML_HEAD + '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/'
|
|
1741
|
+
'core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" '
|
|
1742
|
+
'xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
|
|
1743
|
+
'<dc:title>%s</dc:title><dc:creator></dc:creator>'
|
|
1744
|
+
'<dcterms:created xsi:type="dcterms:W3CDTF">%s</dcterms:created>'
|
|
1745
|
+
'<dcterms:modified xsi:type="dcterms:W3CDTF">%s</dcterms:modified></cp:coreProperties>') % (
|
|
1746
|
+
x(re.sub(r"[*_`]", "", doc_title)), now, now)
|
|
1747
|
+
app = (XML_HEAD + '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">'
|
|
1748
|
+
'<Application>md_to_pptx.py</Application><Slides>%d</Slides><Notes>%d</Notes></Properties>') % (
|
|
1749
|
+
len(slides), n_notes)
|
|
1750
|
+
pres_props = XML_HEAD + '<p:presentationPr %s/>' % NS
|
|
1751
|
+
view_props = (XML_HEAD + '<p:viewPr %s><p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="94660"/>'
|
|
1752
|
+
'</p:normalViewPr><p:gridSpacing cx="76200" cy="76200"/></p:viewPr>') % NS
|
|
1753
|
+
table_styles = (XML_HEAD + '<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
|
1754
|
+
'def="{2D5ABB26-0587-4C30-8999-92F81FD0307C}"/>')
|
|
1755
|
+
title_brand = dict(brand, primary=brand.get("primary_for_text", brand["primary"]))
|
|
1756
|
+
|
|
1757
|
+
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
|
1758
|
+
z.writestr("[Content_Types].xml", content_types)
|
|
1759
|
+
z.writestr("_rels/.rels", root_rels)
|
|
1760
|
+
z.writestr("docProps/core.xml", core)
|
|
1761
|
+
z.writestr("docProps/app.xml", app)
|
|
1762
|
+
z.writestr("ppt/presentation.xml", presentation)
|
|
1763
|
+
z.writestr("ppt/_rels/presentation.xml.rels", rels_xml(pres_rels))
|
|
1764
|
+
z.writestr("ppt/presProps.xml", pres_props)
|
|
1765
|
+
z.writestr("ppt/viewProps.xml", view_props)
|
|
1766
|
+
z.writestr("ppt/tableStyles.xml", table_styles)
|
|
1767
|
+
z.writestr("ppt/theme/theme1.xml", theme_xml(title_brand, "Deck"))
|
|
1768
|
+
z.writestr("ppt/slideMasters/slideMaster1.xml", master_xml(width))
|
|
1769
|
+
z.writestr("ppt/slideMasters/_rels/slideMaster1.xml.rels", rels_xml([
|
|
1770
|
+
("rId1", "slideLayout", "../slideLayouts/slideLayout1.xml", False),
|
|
1771
|
+
("rId2", "slideLayout", "../slideLayouts/slideLayout2.xml", False),
|
|
1772
|
+
("rId3", "theme", "../theme/theme1.xml", False)]))
|
|
1773
|
+
for k in (1, 2):
|
|
1774
|
+
z.writestr("ppt/slideLayouts/slideLayout%d.xml" % k, layout_xml(k, width))
|
|
1775
|
+
z.writestr("ppt/slideLayouts/_rels/slideLayout%d.xml.rels" % k, rels_xml([
|
|
1776
|
+
("rId1", "slideMaster", "../slideMasters/slideMaster1.xml", False)]))
|
|
1777
|
+
if n_notes:
|
|
1778
|
+
z.writestr("ppt/theme/theme2.xml", theme_xml(title_brand, "Deck notes"))
|
|
1779
|
+
z.writestr("ppt/notesMasters/notesMaster1.xml", notes_master_xml(width))
|
|
1780
|
+
z.writestr("ppt/notesMasters/_rels/notesMaster1.xml.rels", rels_xml([
|
|
1781
|
+
("rId1", "theme", "../theme/theme2.xml", False)]))
|
|
1782
|
+
for i, s in enumerate(slides, 1):
|
|
1783
|
+
if s.notes:
|
|
1784
|
+
s.add_rel("notesSlide", "../notesSlides/notesSlide%d.xml" % i)
|
|
1785
|
+
z.writestr("ppt/notesSlides/notesSlide%d.xml" % i, notes_xml(s.notes, width))
|
|
1786
|
+
z.writestr("ppt/notesSlides/_rels/notesSlide%d.xml.rels" % i, rels_xml([
|
|
1787
|
+
("rId1", "notesMaster", "../notesMasters/notesMaster1.xml", False),
|
|
1788
|
+
("rId2", "slide", "../slides/slide%d.xml" % i, False)]))
|
|
1789
|
+
z.writestr("ppt/slides/slide%d.xml" % i, s.xml())
|
|
1790
|
+
z.writestr("ppt/slides/_rels/slide%d.xml.rels" % i, rels_xml(s.rels))
|
|
1791
|
+
for name, data in deck.media:
|
|
1792
|
+
z.writestr(name, data)
|
|
1793
|
+
|
|
1794
|
+
n_parts, n_rels = verify(out)
|
|
1795
|
+
|
|
1796
|
+
# ---- report
|
|
1797
|
+
c = deck.counts
|
|
1798
|
+
n_cont = sum(1 for p in plan if p["title"].endswith(" (continued)") and p["kind"] == "content")
|
|
1799
|
+
parts = ["%d slide%s" % (len(slides), "" if len(slides) == 1 else "s")]
|
|
1800
|
+
if n_cont:
|
|
1801
|
+
parts.append("%d of them continued" % n_cont)
|
|
1802
|
+
for k in ("tables", "images", "code blocks", "links"):
|
|
1803
|
+
if c[k]:
|
|
1804
|
+
parts.append("%d %s" % (c[k], k[:-1] if c[k] == 1 else k))
|
|
1805
|
+
parts.append("speaker notes on %d" % n_notes)
|
|
1806
|
+
print("wrote %s (%s; %s; brand: %s)" % (out, ", ".join(parts), aspect,
|
|
1807
|
+
"%s / %s" % (brand["heading_font"], brand["body_font"]) if branded else "no"))
|
|
1808
|
+
print(" checked: %d XML parts parse, %d relationships resolve, every part has a content type" % (
|
|
1809
|
+
n_parts, n_rels))
|
|
1810
|
+
for n, item in enumerate(plan, 1):
|
|
1811
|
+
bits = []
|
|
1812
|
+
for k in ("table", "image", "code"):
|
|
1813
|
+
m = sum(1 for u in item["units"] if u["kind"] == k)
|
|
1814
|
+
if m:
|
|
1815
|
+
bits.append("%s%s" % (k, " x%d" % m if m > 1 else ""))
|
|
1816
|
+
if slides[n - 1].notes:
|
|
1817
|
+
bits.append("notes")
|
|
1818
|
+
label = {"title": "title", "section": "section"}.get(item["kind"], "")
|
|
1819
|
+
print(" %2d. %s%s%s" % (n, re.sub(r"[*_`]", "", item["title"]),
|
|
1820
|
+
" [%s]" % label if label else "", " [%s]" % ", ".join(bits) if bits else ""))
|
|
1821
|
+
if WARNINGS:
|
|
1822
|
+
print("%d warning(s): the items above were dropped, split or changed; tell the user before sharing"
|
|
1823
|
+
% len(WARNINGS))
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def main(argv):
|
|
1827
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
1828
|
+
ap.add_argument("input", help="Markdown file, or - for stdin")
|
|
1829
|
+
ap.add_argument("-o", "--output", required=True, help="output .pptx path")
|
|
1830
|
+
ap.add_argument("--brand", help="brand.json (fonts.heading/body, colors.primary/accent/text)")
|
|
1831
|
+
ap.add_argument("--title", help="title slide text (default: front matter title, then the first # heading)")
|
|
1832
|
+
ap.add_argument("--aspect", choices=sorted(SLIDE_W), default="16:9", help="slide shape (default 16:9)")
|
|
1833
|
+
a = ap.parse_args(argv)
|
|
1834
|
+
if not a.output.lower().endswith(".pptx"):
|
|
1835
|
+
die("output must end in .pptx: %s" % a.output)
|
|
1836
|
+
convert(a.input, a.output, a.brand, a.title, a.aspect)
|
|
1837
|
+
return 0
|
|
1838
|
+
|
|
1839
|
+
|
|
1840
|
+
if __name__ == "__main__":
|
|
1841
|
+
sys.exit(main(sys.argv[1:]))
|