From 4d74ee576c86e4db7fb9c39257d8aa8805b8a7ae Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Fri, 14 Aug 2026 00:02:15 +0800 Subject: [PATCH] Sync third-party and MCP marketplace plugins Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed. --- config/external-sources.lock.json | 20 +- .../plugins/caveman/THIRD_PARTY_SOURCE.json | 4 +- .../plugins/grill-me/THIRD_PARTY_SOURCE.json | 4 +- .../plugins/grill-me/skills/grill-me/SKILL.md | 1 + .../plugins/mcp-playwright/MCP_SOURCE.json | 2 +- .../next-skills/THIRD_PARTY_SOURCE.json | 4 +- plugins/codex/plugins/ppt-master/README.md | 14 +- .../ppt-master/THIRD_PARTY_SOURCE.json | 4 +- .../ppt-master/skills/ppt-master/SKILL.md | 2 +- .../references/artifact-ownership.md | 15 +- .../ppt-master/references/executor-base.md | 10 +- .../ppt-master/references/executor-chart.md | 6 + .../ppt-master/references/executor-image.md | 11 +- .../ppt-master/references/image-base.md | 11 +- .../ppt-master/references/image-generator.md | 2 +- .../references/image-layout-patterns.md | 8 +- .../references/image-layout-spec.md | 28 +- .../references/native-data-interface.md | 31 +- .../ppt-master/references/native-formula.md | 102 ++ .../references/native-hyperlinks.md | 100 ++ .../references/native-shape-authoring.md | 2 +- .../references/shared-standards-core.md | 3 +- .../ppt-master/references/strategist-image.md | 39 +- .../ppt-master/references/strategist.md | 32 +- .../references/svg-image-embedding.md | 8 +- .../skills/ppt-master/scripts/README.md | 9 +- .../ppt-master/scripts/confirm_ui/server.py | 24 +- .../scripts/confirm_ui/static/app.js | 14 - .../scripts/confirm_ui/static/catalogs.json | 38 - .../ppt-master/scripts/docs/confirm_ui.md | 21 +- .../ppt-master/scripts/docs/conversion.md | 7 + .../skills/ppt-master/scripts/docs/image.md | 40 +- .../ppt-master/scripts/docs/svg-pipeline.md | 23 +- .../ppt-master/scripts/hyperlink_contract.py | 399 +++++ .../ppt-master/scripts/pptx_opc_validation.py | 136 ++ .../scripts/pptx_to_svg/hyperlinks.py | 93 ++ .../scripts/pptx_to_svg/ooxml_loader.py | 5 + .../scripts/pptx_to_svg/shape_walker.py | 28 +- .../scripts/pptx_to_svg/slide_to_svg.py | 57 +- .../scripts/pptx_to_svg/tbl_to_svg.py | 6 + .../scripts/pptx_to_svg/txbody_to_svg.py | 57 +- .../scripts/prompt_audit_manifest.json | 73 +- .../scripts/source_to_md/ppt_to_md.py | 82 +- .../scripts/svg_finalize/flatten_tspan.py | 58 +- .../ppt-master/scripts/svg_quality/checker.py | 62 +- .../scripts/svg_quality/svg_contracts.py | 2 +- .../scripts/svg_to_pptx/animation_config.py | 14 + .../scripts/svg_to_pptx/drawingml/context.py | 3 + .../svg_to_pptx/drawingml/converter.py | 173 ++- .../scripts/svg_to_pptx/drawingml/elements.py | 193 ++- .../svg_to_pptx/drawingml/hyperlinks.py | 162 ++ .../scripts/svg_to_pptx/drawingml/utils.py | 4 +- .../svg_to_pptx/native_objects/__init__.py | 66 +- .../svg_to_pptx/native_objects/chart_data.py | 156 +- .../svg_to_pptx/native_objects/chart_style.py | 18 + .../svg_to_pptx/native_objects/chart_xml.py | 34 +- .../svg_to_pptx/native_objects/formula.py | 243 +++ .../native_objects/formula_compiler.py | 1349 +++++++++++++++++ .../native_objects/inline_formula.py | 302 ++++ .../native_objects/marker_attributes.py | 4 +- .../native_objects/marker_common.py | 10 +- .../native_objects/marker_status.py | 8 +- .../svg_to_pptx/pptx_package/builder.py | 127 +- .../pptx_package/template_validation.py | 2 + .../scripts/template_fill_pptx/applier.py | 261 +++- .../templates/design_spec_reference.md | 6 +- .../templates/scaffolds/design_spec.md | 2 +- .../ppt-master/workflows/generate-pptx.md | 46 +- .../workflows/governance/failure-recovery.md | 2 +- .../workflows/native-enhance-pptx.md | 7 +- .../workflows/profiles/quick-generate.md | 48 +- .../workflows/stages/resume-execute.md | 4 +- .../workflows/template-fill-pptx.md | 6 + .../plugins/shadcn/THIRD_PARTY_SOURCE.json | 4 +- 74 files changed, 4482 insertions(+), 469 deletions(-) create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-formula.md create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-hyperlinks.md create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/hyperlink_contract.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/hyperlinks.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/hyperlinks.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula_compiler.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/inline_formula.py diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index ebf7082e..1778305c 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -42,8 +42,8 @@ "repo": "https://github.com/JuliusBrussee/caveman.git", "ref": "main", "adapter": "codex-plugin", - "commit": "613d7f0402fb51bdde0edb6b01853b391a06b765", - "syncedAt": "2026-08-13T08:38:10Z" + "commit": "c72984e4392c7a154e55c11dbf445f01ce5c35d4", + "syncedAt": "2026-08-13T15:59:59Z" }, { "id": "taste-skill", @@ -60,8 +60,8 @@ "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", "adapter": "claude-skill", - "commit": "a85299a9edd2a961e32f01ced86963a852652bd2", - "syncedAt": "2026-08-13T08:38:10Z" + "commit": "dec288ed71d60f6cb090f60f94bc648eaf1c528b", + "syncedAt": "2026-08-13T15:59:59Z" }, { "id": "frontend-slides", @@ -96,8 +96,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "bcc5762d6c770025f37ca459f5d9ea96f1783fc1", - "syncedAt": "2026-08-13T08:38:10Z" + "commit": "e8323bfaee249cffe1301ec40fca5875eb544d46", + "syncedAt": "2026-08-13T15:59:59Z" }, { "id": "grill-me", @@ -105,8 +105,8 @@ "repo": "https://github.com/mattpocock/skills.git", "ref": "main", "adapter": "skill-collection", - "commit": "84fdeffd12f2ee307994d1eb6feb48173b6e0502", - "syncedAt": "2026-08-13T08:38:10Z" + "commit": "8b78b531ab965735c5dc74f6f7a219e1e37326df", + "syncedAt": "2026-08-13T15:59:59Z" }, { "id": "next-skills", @@ -114,8 +114,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "fef4c28bba9f080fce9687a7a8ee7ac3784de57e", - "syncedAt": "2026-08-13T08:38:10Z" + "commit": "9e1a290955a9177a97a1d44f5305b1236410216a", + "syncedAt": "2026-08-13T15:59:59Z" } ] } diff --git a/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json index 39e6520d..ff39e465 100644 --- a/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "caveman", "repo": "https://github.com/JuliusBrussee/caveman.git", "ref": "main", - "commit": "613d7f0402fb51bdde0edb6b01853b391a06b765", + "commit": "c72984e4392c7a154e55c11dbf445f01ce5c35d4", "adapter": "codex-plugin", "sourcePath": "plugins/caveman", - "syncedAt": "2026-08-13T08:38:10Z" + "syncedAt": "2026-08-13T15:59:59Z" } diff --git a/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json index 58979fff..f3499b6b 100644 --- a/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/grill-me/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "grill-me", "repo": "https://github.com/mattpocock/skills.git", "ref": "main", - "commit": "84fdeffd12f2ee307994d1eb6feb48173b6e0502", + "commit": "8b78b531ab965735c5dc74f6f7a219e1e37326df", "adapter": "skill-collection", "sourcePath": "skills/productivity", - "syncedAt": "2026-08-13T08:38:10Z" + "syncedAt": "2026-08-13T15:59:59Z" } diff --git a/plugins/codex/plugins/grill-me/skills/grill-me/SKILL.md b/plugins/codex/plugins/grill-me/skills/grill-me/SKILL.md index c50d919b..5913c282 100644 --- a/plugins/codex/plugins/grill-me/skills/grill-me/SKILL.md +++ b/plugins/codex/plugins/grill-me/skills/grill-me/SKILL.md @@ -1,6 +1,7 @@ --- name: grill-me description: "在用户明确要求严格追问、压力测试或彻底澄清计划、设计、决策或想法时使用。" +disable-model-invocation: true --- Run a `/grilling` session. diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 2e6ac0e2..aecbc6e1 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-08-13T08:44:49Z" + "syncedAt": "2026-08-13T16:02:14Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index 4a330a8e..860c3372 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "fef4c28bba9f080fce9687a7a8ee7ac3784de57e", + "commit": "9e1a290955a9177a97a1d44f5305b1236410216a", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-08-13T08:38:10Z" + "syncedAt": "2026-08-13T15:59:59Z" } diff --git a/plugins/codex/plugins/ppt-master/README.md b/plugins/codex/plugins/ppt-master/README.md index 52c4ca74..c843ec2a 100644 --- a/plugins/codex/plugins/ppt-master/README.md +++ b/plugins/codex/plugins/ppt-master/README.md @@ -55,7 +55,11 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this Roadmap

-

Download the new narrated Attention Is All You Need deck — play it in PowerPoint and every slide reads itself out loud. That's just the tip of what PPT Master can do.

+

+ The narrated deck and gallery below were generated in May 2026 with Claude Opus 4.7 + gpt-image-2 — one pass each, no manual polish. +

+ +

Download the narrated Attention Is All You Need deck — play it in PowerPoint and every slide reads itself out loud. That's just the tip of what PPT Master can do.

@@ -95,7 +99,7 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this

- All examples above were generated in a single pass, with no manual polish (Claude Opus 4.7 + gpt-image-2). Downloading any .pptx and opening it in PowerPoint is the fastest way to see what it can really do.
Flip through all examples online → · examples/ directory · Why PPT Master?
+ Downloading any .pptx and opening it in PowerPoint is the fastest way to see what it can really do.
Flip through all examples online → · examples/ directory · Why PPT Master?

--- @@ -254,7 +258,7 @@ Then install dependencies: pip install -r requirements.txt ``` -**Option B — Download ZIP** (no Git required; best for a quick trial): click **Code → Download ZIP** on the [GitHub page](https://github.com/hugohe3/ppt-master), then unzip, and install dependencies with `pip install -r requirements.txt`. A ZIP has no Git history, so it can't `git pull` — see Updating Later. If that download is too large or fails, grab the skill-only package `ppt-master-skill-*.zip` (~50 MB, fully functional but without the bundled example decks) from the [Releases](https://github.com/hugohe3/ppt-master/releases) page instead. +**Option B — Download ZIP** (no Git required; best for a quick trial): click **Code → Download ZIP** on the [GitHub page](https://github.com/hugohe3/ppt-master), then unzip, and install dependencies with `pip install -r requirements.txt`. A ZIP has no Git history, so it can't `git pull` — see Updating Later. If that download is too large or fails, grab the skill-only package `ppt-master-skill-*.zip` (~56 MB, fully functional but without the bundled example decks) from the [Releases](https://github.com/hugohe3/ppt-master/releases) page instead. #### Updating Later @@ -322,9 +326,9 @@ The AI handles everything — content analysis, visual design, SVG generation, a You: Quickly generate a 5-page deck from projects/q3-report/sources/report.pdf — no need to confirm with me ``` -Whatever you state explicitly is followed; whatever you leave unspecified the agent decides on its own instead of asking. It still converts sources, fills factual gaps, applies the shared visual baseline, and uses images/icons/native shapes/charts/tables/formulas as needed — it drops interaction and durable planning, not presentation capability. It is one-pass and non-resumable, and there is no `svg_final/` preview. Full guide → [Quick mode](./docs/getting-started.md#quick-mode). +Whatever you state explicitly is followed; whatever you leave unspecified the agent decides on its own instead of asking. It still converts sources, fills factual gaps, applies the shared visual baseline, and uses images/icons/native shapes/charts/tables/PowerPoint-native inline or block formulas as needed — it drops interaction and durable planning, not presentation capability. It is one-pass and non-resumable, and there is no `svg_final/` preview. Full guide → [Quick mode](./docs/getting-started.md#quick-mode). -> **Output:** The SVG pipeline has one PPTX converter: it reads `svg_output/` and writes a directly editable native DrawingML deck to `exports/_.pptx`. The default Generate flow runs `finalize_svg.py` and produces self-contained previews in `svg_final/`; PowerPoint's manual **Convert to Shape** command is outside the supported contract. Explicit [quick generation](./skills/ppt-master/workflows/profiles/quick-generate.md) skips Strategist, confirmation, `design_spec.md`, `spec_lock.md`, and `finalize_svg.py`: whatever you state explicitly is followed, and whatever you leave unspecified the agent decides directly in one active context. It still converts sources, researches factual gaps, applies shared mode/style/aesthetic guidance, prepares required images/icons/formulas, considers native shapes and data visualizations, hand-authors SVG, passes the lockless Quick final quality check, and exports the final PPTX. It writes no substitute plan and cannot resume after context loss. Ordinary export capabilities remain available as needed, including native chart/table replacement, notes, motion, narration, and diagnostics; notes, custom object animation, and narration start off, and the agent may enable them when the request or deck needs them. A default-path Quick export writes the normal postflight report and snapshots `svg_output/` to `backup//svg_output/`; an explicit output path keeps the ordinary no-backup behavior. By default charts and tables export as individually editable SVG-derived DrawingML shapes, which prioritize cross-app visual consistency. Pass `--native-charts-and-tables` to replace eligible groups with PowerPoint-native Chart/Table objects backed by data, which provide **Edit Data** and object-specific controls but may render differently across apps; this variant is saved as `exports/__native_charts_tables.pptx`. Both chart/table export variants are editable—the distinction is the PowerPoint object model, not editability itself. +> **Output:** The SVG pipeline has one PPTX converter: it reads `svg_output/` and writes a directly editable native DrawingML deck to `exports/_.pptx`. The default Generate flow runs `finalize_svg.py` and produces self-contained previews in `svg_final/`; PowerPoint's manual **Convert to Shape** command is outside the supported contract. Explicit [quick generation](./skills/ppt-master/workflows/profiles/quick-generate.md) skips Strategist, confirmation, `design_spec.md`, `spec_lock.md`, and `finalize_svg.py`: whatever you state explicitly is followed, and whatever you leave unspecified the agent decides directly in one active context. It still converts sources, researches factual gaps, applies shared mode/style/aesthetic guidance, prepares required images/icons, authors formulas as native inline or block markers, considers native shapes and data visualizations, hand-authors SVG, passes the lockless Quick final quality check, and exports the final PPTX. It writes no substitute plan and cannot resume after context loss. Formula markers compile their LaTeX payload to editable OMML for PowerPoint 2010+; block groups and inline `` runs keep ordinary SVG previews that are replaced during export. Formula rendering and editability in Keynote, WPS, LibreOffice, and other non-PowerPoint clients are not part of this contract. Ordinary export capabilities remain available as needed, including native chart/table replacement, notes, motion, narration, and diagnostics; notes, custom object animation, and narration start off, and the agent may enable them when the request or deck needs them. A default-path Quick export writes the normal postflight report and snapshots `svg_output/` to `backup//svg_output/`; an explicit output path keeps the ordinary no-backup behavior. By default charts and tables export as individually editable SVG-derived DrawingML shapes, which prioritize cross-app visual consistency. Pass `--native-charts-and-tables` to replace eligible groups with PowerPoint-native Chart/Table objects backed by data, which provide **Edit Data** and object-specific controls but may render differently across apps; this variant is saved as `exports/__native_charts_tables.pptx`. Both chart/table export variants are editable—the distinction is the PowerPoint object model, not editability itself. > **Already have a `.pptx` you want to reuse?** Hand the AI that deck plus your material and ask it to "fill this deck with the new content" — it fills text, table, and chart data into your existing design and exports only the pages you pick, staying natively editable. See the [FAQ](./docs/faq.md) and [template-fill workflow](./skills/ppt-master/workflows/template-fill-pptx.md). diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index e8ff7509..6e8f740f 100644 --- a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ppt-master", "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", - "commit": "bcc5762d6c770025f37ca459f5d9ea96f1783fc1", + "commit": "e8323bfaee249cffe1301ec40fca5875eb544d46", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-08-13T08:38:10Z" + "syncedAt": "2026-08-13T15:59:59Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md index 68c8d136..5e4ab293 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md @@ -2,7 +2,7 @@ name: ppt-master description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶段演示文稿生成工作流。" metadata: - version: "4.5.0" + version: "4.7.0" copyright: "Copyright (c) 2025-2026 Hugo He" license: "MIT" official_repository: "https://github.com/hugohe3/ppt-master" diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md index 7a65d1b4..a478d2a5 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md @@ -7,10 +7,11 @@ profile may explicitly omit an artifact without erasing its facts. **Quick Generate projection**: Quick omits confirmation, Design Spec, and lock. Its current main agent reads source/analysis facts, keeps routine decisions in -active context, and prepares the selected images/icons/formulas plus required -operational manifests before SVG authoring. It may also realize charts, tables, -native shapes, and other ordinary authoring capabilities directly from that -context. Exact template workspace roots supplied for the run are validated and +active context, and prepares the selected images/icons plus required +operational manifests before SVG authoring. It realizes native formulas directly +from exact mathematical content under [`native-formula.md`](./native-formula.md), +and may also realize charts, tables, native shapes, and other ordinary authoring +capabilities directly from that context. Exact template workspace roots supplied for the run are validated and installed directly; Quick reads only that project-local state and creates no Confirm UI selection artifacts. Those artifacts retain their factual/provenance roles. Quick writes the same final SVG quality provenance and package postflight @@ -38,8 +39,8 @@ history, or resumable planning state. Context loss restarts the Quick run. | `spec_lock.md` | Execution anchor and routing contract | Machine-readable stable color/type roles, icons, images, page rhythm, charts, `template_reuse_scope`, and the route's PowerPoint structure mode; mirror/layout template routes additionally own input prototypes, the Master roster, and the complete page-to-Master/Layout mapping | Strategist authors the route-specific anchors from the audited Design Spec plus current project/page/template context. Executor retains the complete lock once per valid execution context; local uncertainty consults that retained copy before the owning Design Spec fragment. Sparse page-local color/font garnish needs no lock row; a recurring semantic role or new adaptive Layout identity requires Strategist repair before reuse. | | `project_manager.py page-context` stdout | Derived on-demand page context | Read-only model-facing anchor set + current-page delta + fingerprints for large references | Use only for explicit diagnostics/telemetry or an unresolved page/template/chart path-SHA projection. Never edit or persist it as a replacement source of truth, and never run it as a routine pre-page gate. `global` is a bounded anchor set, not a whitelist. `reference_set` carries path/SHA/load policy but never appends reference payloads. | | `analysis/page-context/P.usage.json` | Derived optional context telemetry | Measured on-demand page-context size plus hashes of owning inputs/references | `page-context --record-usage` deterministically replaces only the invoked page's snapshot; `page-context-report` summarizes existing snapshots. Telemetry may be partial. Use token data to evaluate context cost, never as content or an execution contract. | -| `images/` | Runtime image pool | User, extracted, AI, web, formula, slice, EMF/WMF assets | Default Step 5 or Quick Generate resource preparation writes here; `analysis/image_analysis.csv` derives from current contents | -| `images/image_prompts.json`, `image_queries.json`, `image_sources.json`, `formula_manifest.json` | Conditional resource contracts | AI/web/formula execution status and provenance | Create only for a triggered path, including Quick. They guide preparation/attribution, never page design. | +| `images/` | Runtime image pool | User, extracted, AI, web, slice, and EMF/WMF assets | Default Step 5 or Quick Generate resource preparation writes here; `analysis/image_analysis.csv` derives from current contents | +| `images/image_prompts.json`, `image_queries.json`, `image_sources.json` | Conditional resource contracts | AI/web execution status and provenance | Create only for a triggered path, including Quick. They guide preparation/attribution, never page design. | | `icons/` | Prepared project icon pool | Bundled icons copied by `icon_sync.py` plus user-provided, template, imported, or custom icon SVGs | SVG authoring may choose any icon in this project-local pool per page; `spec_lock.icons.inventory` indexes the default plan's curated synced bundled pool rather than assigning page usage or defining an exhaustive whitelist. Exporter global fallback is legacy compatibility only. | | `${SKILL_DIR}/templates/{brands,styles,layouts,decks}/*_index.json` | Library discovery indexes | The complete registered option source for Default Stage-1 template selection and chat listing | The UI server or chat branch reads these indexes only to populate the Stage-1 choice, after the communication recommendation is authored. Never scan kind directories to add options or use index summaries as Stage-1 planning evidence. Derive a library root from kind + entry id. Exact unregistered roots remain explicit inputs. Quick does not read the catalog. | | `templates/` | Project template reference | Stage-1-confirmed non-free selection or Quick direct-input installed specs, one file per selected workspace, optional Layout/Deck SVG prototypes, and non-image assets | Default template-aware Strategist work from Stage 2 onward, Quick's current agent before direct authoring, and every later role read this project-local state only, never the library/external installation root. The active planner reads every installed template Design Spec and an actual SVG roster only for Layout/Deck; Brand and Style are intentionally roster-free. Continuous Executor reuses that context; fresh Executor reads the Design Spec once and each selected complete SVG, when any, only before first use or after its SHA changes. | @@ -54,7 +55,7 @@ history, or resumable planning state. Context loss restarts the Quick run. | `confirm_ui/template_options.json`, `template_selection.json`, `template_handoff.json` | Default UI template-selection sidecar | Agent-authored candidate input, user-confirmed selection written beside the Stage-1 result, and agent-authored installation/free-design completion handoff | Step 3 writes options without launching UI. The Stage-1 submission writes `template_selection.json` alongside `result.json`; template choices never enter the Strategist contract. After installation/free-design closure, `--complete-template-selection` writes the bound handoff. Stage 2 is exposed only after that handoff and a fresh recommendation. Chat/delegated flows retain equivalent state without fabricating UI receipts; Quick creates none. | | `confirm_ui/recommendations.stage1.json`, `.stage2.json` | Confirmation proposals | Template-independent communication contract, then template-aware complete solution plus production mechanics | Author the Stage-1 communication recommendation without using candidate indexes or workspaces as evidence; candidate display state may be prepared independently. Its page confirms communication plus template mode/selection in one submission. Create Stage 2 only after the selection is installed or free design closes and the handoff/equivalent state is ready. `template_application` decides only how to use installed project-local state. The active unconfirmed stage may be overwritten; normal progression leaves confirmed Stage 1 intact. | | `confirm_ui/result.json` | Confirmation result | Persisted user-confirmed input evidence | Generate Step 4 reads the final object once into active context; Strategist consumes it completely into `design_spec.md`. Normal downstream work does not reopen it; fresh recovery may read it once when no retained final state exists. | -| `svg_output/` | Page-design author source | Main-agent handwritten SVG pages containing the complete visible design | Quality checker and native PPTX export read this as the canonical visual/page-layout source; templates and locks do not add missing visible objects at export | +| `svg_output/` | Page-design author source | Main-agent handwritten SVG pages containing the complete visible design, including each native formula's exact LaTeX marker and ordinary SVG preview | Quality checker and native PPTX export read this as the canonical visual/page-layout source; templates and locks do not add missing visible objects at export. Formula export replaces only its explicit marker subtree under [`native-formula.md`](./native-formula.md). | | `notes/total.md` | Conditional speaker-note source | Complete notes before splitting | Step 6 writes only when the effective Speaker Notes outcome is enabled; Step 7.1 splits | | `notes/slide_*.md` | Conditional split notes | Per-slide notes generated from `total.md` | Derived by `total_md_split.py` only when speaker notes are enabled | | `svg_final/` | Default-only derived visual preview | Self-contained post-processed SVGs that may be opened directly or inserted as SVG pictures | Default rebuilds it from `svg_output/` with `finalize_svg.py`; Quick omits it. Never use it as a supported PPTX source. | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md index 9fab0d03..4e33f243 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md @@ -12,7 +12,9 @@ Always-loaded Executor authority for flat SVG page authoring and behavior shared | The mandatory per-page Structure decision finds qualitative geometry | [`executor-structure.md`](./executor-structure.md) | | Any semantic cell grid, including a table-family reference | [`executor-table.md`](./executor-table.md) | | A page will use a preset pattern fill or an independent object is explicitly selected as native-ready | [`native-data-interface.md`](./native-data-interface.md) before emitting the pattern or replacement metadata | -| Any image/formula | [`executor-image.md`](./executor-image.md) + [`image-layout-spec.md`](./image-layout-spec.md) + [`image-layout-patterns.md`](./image-layout-patterns.md) + [`svg-image-embedding.md`](./svg-image-embedding.md) | +| Any image | [`executor-image.md`](./executor-image.md) + [`image-layout-spec.md`](./image-layout-spec.md) + [`image-layout-patterns.md`](./image-layout-patterns.md) + [`svg-image-embedding.md`](./svg-image-embedding.md) | +| Any nontrivial mathematical expression | [`native-formula.md`](./native-formula.md) | +| Any external or same-deck click hyperlink | [`native-hyperlinks.md`](./native-hyperlinks.md) | | Any placed image is `Status: Sourced` or its filename has an `image_sources.json` record | [`executor-web-image.md`](./executor-web-image.md), after `executor-image.md` | | Effective Speaker Notes outcome is enabled after all SVG pages pass | [`executor-notes.md`](./executor-notes.md) | @@ -20,7 +22,7 @@ Evaluate branches from each object's actual information model, not only from a C > Narrative skeleton and visual aesthetic come from the locked values routed through the [`modes/`](./modes/_index.md) and [`visual-styles/`](./visual-styles/_index.md) indexes. Executor reads one locked preset file or only the exact `*_references` of a custom synthesis; an unreferenced novel custom reads none. [`shared-standards-core.md`](./shared-standards-core.md) supplies the technical boundary plus the fallback visual-quality and leading defaults when those authorities are silent. -**Hard rule — Shape-first page authority**: Every visible object intended for the exported slide MUST exist in the final page SVG or be explicitly referenced by it. Templates and `spec_lock.md` guide construction; they are not export-time overlays for missing visible content. Optional native Chart/Table metadata belongs to an independently selected object and never replaces this visible fallback during authoring; [`native-data-interface.md`](./native-data-interface.md) alone defines that metadata and its export activation. +**Hard rule — Shape-first page authority**: Every visible object intended for the exported slide MUST exist in the final page SVG or be explicitly referenced by it. Templates and `spec_lock.md` guide construction; they are not export-time overlays for missing visible content. Optional native Chart/Table metadata belongs to an independently selected object and never replaces this visible fallback during authoring; [`native-data-interface.md`](./native-data-interface.md) alone defines that metadata and its export activation. Native formula markers require a matching SVG preview; export replaces only it under [`native-formula.md`](./native-formula.md). **Hard rule — flat PowerPoint structure**: Free-design, brand-only, Style-only, and every `template_reuse_scope: style` project use `pptx_structure.mode: flat`: write no root Master/Layout identity, `data-pptx-layer`, or `data-pptx-placeholder`; every visible object remains Slide-local, and the root declares exactly one canonical `data-pptx-page-role` (`cover` / `toc` / `section` / `content` / `ending`). A Style workspace supplies reusable communication/design direction, composition rhythm, and information-expression defaults without page prototypes. Its identity-adjacent color, typography, icon, and image defaults yield to the final Brand/Deck identity and confirmed project lock. When a Style is installed alongside Layout/Deck, it changes only Direction / method and follows the resolved non-Style structure route. Export materializes one clean project-owned Master plus one Blank Layout from the current lock. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by specialized metadata; the marked element uses a stable unique `id`. See [`semantic-svg.md`](./semantic-svg.md). @@ -115,7 +117,7 @@ Apply the content-vs-expression contract above within the selected reading mode. - **Sparse display-size exception**: a short non-structural Hero/Display element may use one undeclared size outside all anchor bands at most twice across the deck without a lock row. The third occurrence makes that size recurring: stop and return to Strategist to name the role in the Design Spec and `spec_lock.md`, then read back and validate the affected fragments before reuse. This exception never applies to titles, body copy, subtitles, annotations, footnotes, captions, data labels, or card copy, and nearby sizes must not be introduced to imitate one recurring treatment. - **Outside-band recovery**: for structural text, reflow geometry and use the declared role band locally. For a sparse display occurrence, keep the unitless value and verify that its deck-wide count remains at most two. Never flatten a justified distinction or add a role merely to silence the checker. Mirror pages preserve exact source typography as inherited input. - Images MUST reference files listed under `images`; no invented filenames -- Formula PNGs are images with `Acquire Via: formula`; place a `Rendered` file only from its listed path, use the normal placeholder for `Needs-Manual`, and never recreate the formula as text. +- For math, load [`native-formula.md`](./native-formula.md): simple notation stays text; one-line structural prose uses inline; matrices, multiline derivations, or standalone high structure use block. Keep exact LaTeX plus preview; never use an image. Return upstream before any derived/accent identity becomes recurring or structural, or when an undeclared display size reaches its third occurrence, then update the retained context under §2.1. Local garnish, same-role `±2`px adjustments, and at most two sparse display-size occurrences need no lock row. Never expand the lock to silence a comparison. New icon acquisition, images, structural fonts, role anchors, and resources keep their preparation/role rules. @@ -298,7 +300,7 @@ test -f "/icons//.svg" ## 5. Font Usage -Read typography from `spec_lock.md`: `_family` → `title_family` / `body_family` → legacy `font_family`; sparse accents follow §2.1 and LaTeX stays PNG. +Read typography from `spec_lock.md`: `_family` → `title_family` / `body_family` → legacy `font_family`; sparse accents follow §2.1. Under [`native-formula.md`](./native-formula.md), blocks use marker style; inline math inherits size / visible solid fill and exports with the project text language in Cambria Math. **Default — locked-stack realization (may vary treatment)**: Express the Design Spec Character Reference through scale, weight, spacing, color, and composition; keep the locked family. Put the common stack on root ``, omit matching descendants, and override at the nearest clear ``, ``, or ``. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-chart.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-chart.md index 54c870f6..630da694 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-chart.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-chart.md @@ -118,6 +118,12 @@ The number of markers must equal the number of promoted chart objects, and each marker must sit under its matching object wrapper. One marker somewhere on a multi-chart page is insufficient. +**Native layout handoff**: for a native-ready classic chart whose authored plot +rectangle must remain fixed, copy that final absolute slide rectangle into +metadata `plot_area`; omit it only for PowerPoint automatic layout. The marker +comment alone does not affect export; the closed schema stays in +[`native-data-interface.md`](./native-data-interface.md) §2. + Technical SVG/PPT constraints remain in [`shared-standards-core.md`](./shared-standards-core.md). --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md index bad57955..0c600054 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md @@ -4,9 +4,9 @@ # Executor Image Branch -Conditional Executor authority for image status handling, placement, crop behavior, formula images, and template-bundled images. +Conditional Executor authority for image status handling, placement, crop behavior, and template-bundled images. -**Trigger**: load for any image/formula in §VIII, the lock, Quick Generate's active-context resource decisions, or a selected template. +**Trigger**: load for any image in §VIII, the lock, Quick Generate's active-context resource decisions, or a selected template. ## 1. Image Handling @@ -19,7 +19,6 @@ Handle images by status; enum and lifecycle: [`svg-image-embedding.md`](svg-imag | **Existing** | User-provided | Reference images directly from `../images/` directory | | **Generated** | Generated by Image_Generator | Reference from `../images/`; manifest-backed files load [`executor-web-image.md`](./executor-web-image.md) | | **Sourced** | Web-acquired by Image_Searcher | Reference from `../images/`. **Read [`image_sources.json`](image-searcher.md) to decide attribution** — load [`executor-web-image.md`](./executor-web-image.md). | -| **Rendered** | Deterministic formula PNG | Reference from `../images/`; use a legal anchor with `meet` (centered default: `xMidYMid meet`) | | **Needs-Manual** | Acquisition or suitability remains unresolved | Default uses a placeholder until Step 7. Quick blocks every required row in this status; file presence alone does not bypass it. | | **Placeholder** | Not yet prepared | Use dashed border placeholder | @@ -38,8 +37,8 @@ must-use, crop/content, and explicit user/template constraints; expression-only changes need no upstream rewrite. **Mandatory — per-page image composition decision**: Using this already-loaded -branch, decide once immediately before geometry on every page containing a -non-formula image and keep the result only in active context: +branch, decide once immediately before geometry on every page containing an +image and keep the result only in active context: `role → direction generator → parent contour → slots/rhythm → crop → image-shape action → labels/overlays → depth/continuity`. Derive the relationship from the communication job, hierarchy, copy, asset ratio/focus, and deck rhythm; @@ -92,5 +91,3 @@ positions and heights must change the source-unit `x`, `y`, `width`, and `height` by the same union-relative mapping, so the gaps remove pixels without rescaling the scene. A compound clip on one `` is pattern `#M1-10`, not a substitute when the objects must remain independently editable or Morphable. - -**Formula images — declared-inference fallback for a missing `no-crop` flag**: rows with `Acquire Via: formula` or `Type: Latex Formula` MUST be treated as no-crop. For a rendered file, use dimensions in this order: current `analysis/image_analysis.csv`, `design_spec.md §VIII`, then `images/formula_manifest.json`. For a `Needs-Manual` row, size the dashed placeholder from the planned dimensions in §VIII, then the manifest; the readiness gate re-analyzes the supplied file and reconciles the container before export. Do not normalize all formulas to one height unless the spec explicitly states that layout choice. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md index ae15b0f3..264bff83 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md @@ -8,7 +8,7 @@ Shared baseline for both acquisition paths. Path-specific behavior lives in the ## 1. Trigger Condition -Active when at least one resource row has `Acquire Via: ai` / `web` / `slice`, or when any §VIII / Quick active-context resource is a pending prepared derivative. Canonical rows with `user` / `formula` / `placeholder` are tracked but skipped by acquisition roles. +Active when at least one resource row has `Acquire Via: ai` / `web` / `slice`, or when any §VIII / Quick active-context resource is a pending prepared derivative. Canonical rows with `user` / `placeholder` are tracked but skipped by acquisition roles. | Mode | Trigger | |---|---| @@ -28,7 +28,7 @@ Default Generate uses Strategist-owned `design_spec.md §VIII` plus its lock pro **Required per non-skipped row**: `Acquire Via` and `Status`. `Reference` is required for every `web` / `slice` row, every newly authored `ai` row, and every prepared derivative regardless of source class. An existing `ai` row whose `Reference` is omitted or blank may continue only through the declared inference in [`image-generator.md`](./image-generator.md) §8; no other path may infer it. -**Quick Generate ownership**: explicit user assets, URLs, and path instructions win. Otherwise the main agent chooses required `user` / `ai` / `web` / `slice` / `formula` rows and AI path `auto`, without interaction. +**Quick Generate ownership**: explicit user assets, URLs, and path instructions win. Otherwise the main agent chooses required `user` / `ai` / `web` / `slice` rows and AI path `auto`, without interaction. **Mandatory — consume the resolved path**: Default consumes Strategist-chosen §VIII rows; Quick resolves once in active context before preparation. This phase never adds or reselects a treatment: @@ -57,7 +57,7 @@ Choosing `none` is valid. Never bake a native treatment into a derivative. ## 3. Path Dispatch -Classify `Reference: Derived from ; treatment=; ...` before `Acquire Via`. Its distinct, non-derived parent must be `user`, `web`, `ai`, or `slice`; reject formula/placeholder parents, chains, cycles, and self-reference. For each Pending row: +Classify `Reference: Derived from ; treatment=; ...` before `Acquire Via`. Its distinct, non-derived parent must be `user`, `web`, `ai`, or `slice`; reject placeholder parents, chains, cycles, and self-reference. For each Pending row: | Row kind / Acquire Via | Load reference | Run | Success status | |---|---|---|---| @@ -67,7 +67,6 @@ Classify `Reference: Derived from ; treatment= Lazy load: an all-`web` deck never reads `image-generator.md`, and vice versa. @@ -78,7 +77,7 @@ Classify `Reference: Derived from ; treatment= `Needs-Manual` is terminal for acquisition, not export readiness. A later > supplied/replaced file must be validated and its row reconciled to -> `Existing`, `Generated`, `Sourced`, or `Rendered` with matching evidence. +> `Existing`, `Generated`, or `Sourced` with matching evidence. > Quick blocks every required row that still says `Needs-Manual`, regardless of > whether an unverified candidate file happens to exist. See > [`image-generator.md`](./image-generator.md) §7. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md index 7591148c..b5054e08 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-generator.md @@ -670,7 +670,7 @@ Triggered automatically when `IMAGE_BACKEND` is not configured (or Path A fails) - Do **not** run `image_gen.py --manifest` in Path B. That command is Path A and may use configured API/proxy backends even when the user confirmed host-native. - Still run `python3 scripts/image_gen.py --render-md project/images/image_prompts.json` so the human-readable sidecar exists without touching any backend. - **Batch for speed, mind the rate**: when the host can run independent tool calls in parallel (e.g. Claude Code issues independent calls concurrently), fire several generations together in modest groups — a few rows at a time (~3–4), not the whole manifest at once — so their latency overlaps without flooding the host's image quota. When the host only runs tools serially, generate one row at a time. This mirrors Path A's default concurrency of 3. -- Outputs **must** land at `project/images/`. Match the Image Resource List dimensions when the host supports arbitrary sizes. Hosts with **fixed native resolutions** (common — e.g. ~1672x941 landscape / ~1086x1448 portrait) generate at the closest native size and backfill the actual pixels into the resource list `Dimensions` column — same convention as formula rows ("actual dimensions from formula manifest") and slice rows ("dimensions filled after slicing"). Do **not** upscale the file to fake the requested size (interpolation adds no detail); minor display-side upscaling (up to ~1.3x in practice) may surface as a non-blocking quality-checker warning and requires no acknowledgement. +- Outputs **must** land at `project/images/`. Match the Image Resource List dimensions when the host supports arbitrary sizes. Hosts with **fixed native resolutions** (common — e.g. ~1672x941 landscape / ~1086x1448 portrait) generate at the closest native size and backfill the actual pixels into the resource list `Dimensions` column, as slice rows do after slicing. Do **not** upscale the file to fake the requested size (interpolation adds no detail); minor display-side upscaling (up to ~1.3x in practice) may surface as a non-blocking quality-checker warning and requires no acknowledgement. - Mark each item's `status` `Generated` in the manifest the moment its file lands — as each completes, not in one pass at the end (so an interrupted batch leaves accurate state) - Executor downstream is path-agnostic — no spec change required between Path A and Path B diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md index 5a401656..eeb3595d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md @@ -1,6 +1,6 @@ -# Image and Formula Layout Pattern Catalog +# Image Layout Pattern Catalog -Compact composition vocabulary for prepared images, illustrations, and rendered formula assets. Use the patterns as options, not as a checklist. +Compact composition vocabulary for prepared images and illustrations. Use the patterns as options, not as a checklist. --- @@ -238,12 +238,10 @@ If motion is enabled, [`animations.md`](./animations.md) owns its implementation | A prepared subject should re-layer over its source | `#A2-03`; keep the base and cutout registered, and insert a native middle layer only when it has a distinct job | | A busy visual needs one focal region | `#M2-05`, or prepared `#A3-01` / `#A3-03` when a native contrast treatment is insufficient | | A visual argument should build across pages | `#C1-01` + `#P2-01` or `#P2-05`; keep the underlying source and frame stable | -| Formula or technical figure needs explanation | `#P1-12` + `#P2-07` / `#P2-03`; use `#P2-04` only when a second cropped detail is useful, and keep explanatory labels native | +| Technical figure needs explanation | `#P1-12` + `#P2-07` / `#P2-03`; use `#P2-04` only when a second cropped detail is useful, and keep explanatory labels native | **Registration boundary**: registration-dependent effects succeed only when their declared coordinate relationship remains exact. Preserve registration for `#M1-10`, `#A2-02`, `#A3-01`, `#M1-08`, `#A2-03`, `#A3-02`, `#A3-03`, and `#M1-11`; `#M1-09` is the intentional exception. **Source-correspondence boundary**: `#P2-04` reuses one exact source but intentionally changes the detail crop, scale, and placement; preserve the selected-region correspondence instead of forcing page-space registration. -**Formula placement**: treat a rendered formula as a prepared visual asset. Use whitespace patterns such as `#P1-11` or `#P1-12` for isolated derivations, `#P1-07`, `#P2-07`, `#P2-03`, or `#P2-04` for annotated formulas, and `#P3-04` or `#P3-03` for comparisons; keep editable explanatory text native. - All compatibility details remain owned by [`shared-standards-core.md`](./shared-standards-core.md) and its routed references. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-spec.md index 6b77bbdb..a02e6588 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-spec.md @@ -2,9 +2,9 @@ # Image Layout Specification -Neutral geometry and review rules for every image or rendered-formula placement. This file calculates the selected composition; it never chooses a resource, pattern, or automatic left/right or top/bottom layout. +Neutral geometry and review rules for every image placement. This file calculates the selected composition; it never chooses a resource, pattern, or automatic left/right or top/bottom layout. -**When to run**: whenever an image or rendered formula will be placed. Use the current page composition to select its region first, then apply the relevant single-item, adjacent, overlay, or multi-item calculation below. +**When to run**: whenever an image will be placed. Use the current page composition to select its region first, then apply the relevant single-item, adjacent, overlay, or multi-item calculation below. --- @@ -78,18 +78,18 @@ Centered fill uses `ax = ay = 0.5`. SVG realization normally maps this to a lega | Need | Geometry | |---|---| -| Complete source, formula, evidence, or edge content | Contain | +| Complete source, evidence, or edge content | Contain | | Region coverage with a focal-safe crop | Fill | | Complete source plus a detail view | One contain placement plus a separately justified crop | | Irregular or repeated source windows | Apply the selected region math first, then load the owning crop/shape reference | --- -## 3. Single Image or Formula +## 3. Single Image Place a standalone item by applying §2 to its selected region. The region itself comes from the page hierarchy; source ratio determines the item geometry inside it, not the page structure. -For an item adjacent to another region, divide only the available selected region. Let `q_item` and `q_other` be positive visual weights for the image/formula and the other content. +For an item adjacent to another region, divide only the available selected region. Let `q_item` and `q_other` be positive visual weights for the image and the other content. ### 3.1 Horizontal adjacency @@ -202,22 +202,7 @@ listed structural role; otherwise use the selected region boundary and omit it. --- -## 5. Rendered Formula Geometry - -Treat a rendered formula as an aspect-ratio source and apply contain within its selected mathematical region. Centering is the default geometric anchor; align to a nearby baseline or relation only when the page composition defines that relationship. - -For `n` vertically stacked formula regions with equal lanes: - -```text -lane_height = (H - (n - 1) × g) / n -lane_y[i] = y0 + i × (lane_height + g) -``` - -Contain each formula independently in its lane. When formulas are visual peers, a common effective scale may improve comparison; otherwise let their selected regions reflect their semantic weight and source ratios. - ---- - -## 6. Composition Checks +## 5. Composition Checks | Check | Required response | |---|---| @@ -232,7 +217,6 @@ Contain each formula independently in its lane. When formulas are visual peers, | Per-item angles vary without a shared direction or deliberate-disorder rule | Use one parent-group angle, a shared clip-shape direction, curve tangents, or a bounded angle rhythm | | The parent contour does not affect silhouette, seam, reveal, or attachment | Remove it or reconstruct the carriers from that contour | | A panel depends on shear, skew, or perspective warping | Replace it with 2D quadrilateral carriers and focal-safe crops | -| Formula symbols become unreadable at the intended viewing size | Enlarge its region or restructure the page | | Gaps, alignments, or overlaps drift without purpose | Recalculate from the shared region and gap values | The final geometry must express the active page hierarchy, preserve the selected resource relationships, and remain valid under the conditionally loaded technical contracts. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md index 5ffdecbd..2536ba11 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-data-interface.md @@ -92,6 +92,11 @@ inside PowerPoint's 32-bit DrawingML coordinate range; `width` and `height` must resolve to at least one EMU. Native table frames must additionally resolve to at least one EMU per resolved row and column. +**Classic plot-area layout**: supported classic charts accept root `plot_area`; +ChartEx rejects it. It contains only finite `x`, `y`, `width`, `height` in +absolute slide px and forms a positive rectangle inside the chart frame. Export +writes `c:manualLayout`; omission keeps automatic layout. + **Validation**: `svg_quality_checker.py` validates replacement marker kind, JSON metadata, bounds/fallback availability, table rows/columns, supported chart type, chart data shape, and any imported fallback baseline before export. @@ -211,6 +216,8 @@ with `unsupported-merge-topology`. `series[].values`. Pie-family charts (`pie`, `doughnut`, `pieOfPie`, and `barOfPie`) must have exactly one series; the exporter assigns per-category slice colors so single-series charts do not collapse into one solid color. +Root `hole_size` is doughnut-only, integer `10..90`, default `75`; +no pie-family rotation or angle field exists. Column and bar charts may set per-point colors with `series[].point_colors` or `series[].pointColors`; the list must match `series[].values` length. Classic category charts may set native PowerPoint data labels with @@ -259,6 +266,8 @@ area charts and OHLC stock charts; arbitrary date-axis source families are not. This contract is not a full `AxisSpec`: logarithmic scales, minor units/gridlines, crossing values, display units, tick skipping, and other unlisted OOXML semantics remain unsupported and fail closed on import. +Single-plot `bar` accepts `category` at left/right and `value` at bottom/top; +`pie`, `doughnut`, and `pieOfPie` / `barOfPie` reject `axes`. **Narrow XY-axis schema**: `scatter` and `bubble` may use a closed `axes` object with only `x` and `y` roles. Both roles have `kind: "value"`; `x.position` @@ -285,19 +294,15 @@ imply full visual-axis parity. required only when the native object must preserve typography that cannot be inferred unambiguously from the visible fallback. -**Chart chrome metadata**: Text that is visually part of the chart must be in -metadata, not only in SVG fallback children; metadata MUST still match visible -fallback chrome. `title` becomes the native chart title on classic charts; it -is not an object name, so use `name` for semantic object naming. `subtitle` -becomes the second rich-text line of that classic chart title. `title`, -`subtitle`, and axis-title values may be strings or objects with `text`, -`font_size`, `font_family`, and `color` when the fallback uses local role -typography. `svg_quality_checker.py` rejects `title`, `subtitle`, or axis-title -metadata whose text is not visible inside the replacement marker's fallback. Direct -`--native-charts-and-tables` export keeps the chart native but omits that inconsistent -chrome with a warning. chartEx keeps PowerPoint's empty `` and emits -the title / subtitle as companion editable text boxes until chartEx rich titles -are validated. Axis +**Chart chrome metadata**: Metadata MUST match fallback chrome. For classic +charts, a string or unbounded-object `title` becomes native `c:title`; +`subtitle` is line two. A title object with complete `x`, `y`, +`width`, and `height` becomes a companion editable text box at those absolute +slide-px bounds; partial bounds or `subtitle` fail. Use `name`, not +`title`, for object naming. `title`, `subtitle`, and axis-title objects may set +`text`, `font_size`, `font_family`, and `color`. The checker rejects title/axis +text absent from the fallback; export omits it with a warning. ChartEx keeps an empty `` and +emits title/subtitle as companion editable text boxes. Axis titles are optional and explicit: use `axis_titles` with `category`, `value`, `x`, `y`, or `secondary_value` keys, or the root aliases `category_axis_title`, `value_axis_title`, `x_axis_title`, `y_axis_title`, and diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-formula.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-formula.md new file mode 100644 index 00000000..97f1853f --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-formula.md @@ -0,0 +1,102 @@ +# Native Formula Specification + +Shared authoring contract for editable PowerPoint math generated from exact +LaTeX, either inline in Slide-local prose or as a standalone block. + +## 1. Trigger and Ownership + +**Trigger**: A page contains structural mathematical notation such as a +fraction, radical, integral, n-ary expression, limit, matrix, delimiter +construction, accent, or complex script. + +| Layer | Ownership | +|---|---| +| Default Strategist | Record exact mathematical content as a delimiter-free LaTeX expression body; do not classify its implementation | +| Default Executor | Decide ordinary text versus inline native math versus block native math, then author the selected marker and SVG preview | +| Active Quick context | Perform both content and authoring responsibilities directly | +| SVG-to-PPTX exporter | Compile marker LaTeX to editable Office Math and replace only the registered preview | + +| Content form | Authoring choice | +|---|---| +| Short variables, percentages, simple assignments, or notation such as `O(n log n)` | Ordinary editable SVG text | +| One-line structural math embedded in prose | Inline native marker | +| Matrix, `cases`, `aligned`, multiline derivation, or standalone high-structure expression | Block native marker | + +The Strategist's `Mathematical content` field does not pre-decide this choice. +Formula handling is not a user-confirmed policy, image resource, manifest, or +`spec_lock.md images` entry. + +--- + +## 2. Canonical Markers + +### 2.1 Inline formula + +```xml + + The ratio aᵢ/bᵢ remains stable. + +``` + +**Hard rule — one leaf run**: Put non-empty, delimiter-free LaTeX directly in +`data-pptx-inline-formula` on a leaf ``. Give that `` one non-empty +direct preview string with no leading/trailing whitespace, no child element, +and no `x`, `y`, `dx`, `dy`, or paragraph-layout metadata; spacing belongs to +the surrounding text. The marker inherits its computed size and visible solid +fill; exported math uses the project text language and Cambria Math. + +**Hard rule — Slide-local ordinary text only**: Do not place an inline marker +inside a structured Layout placeholder, a Master/Layout layer, imported +preserved `txBody`, geometry transport subtree, another inline marker, or any +`data-pptx-replace-with` subtree. Export keeps the surrounding text runs in the +same `a:p` and replaces only the marker run with `a14:m > m:oMath`. + +### 2.2 Block formula + +```xml + + + {"latex":"\\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}", + "display":"block","font_size":42,"color":"#173B57","align":"center"} + + (-b ± √(b²−4ac)) / 2a + +``` + +**Hard rule — block metadata is truth**: Write one direct +`` child with non-empty delimiter-free +`latex`, `display: block`, `font_size` in `(0, 400]`, a visible `color`, and +`align: left|center|right`. Give the group finite `data-pptx-x/y`, positive +`data-pptx-width/height`, and matching root-coordinate `data-pptx-bounds`. +Export replaces the complete group with `a14:m > m:oMathPara > m:oMath`. + +**Hard rule — preview is SVG, never fallback**: Make every marker preview +semantically equivalent with ordinary SVG text/shapes/lines/paths. Do not use +``, ``, visible raw LaTeX, or another runtime renderer. +The exporter discards the registered preview and emits no picture branch. + +--- + +## 3. Source, Failure, and Validation + +**Supported subset**: basic text, numbers, operators, Greek/symbol commands, +fractions, radicals, scripts, `\sum` / `\prod` / `\int` with limits, `\left` / +`\right` delimiters, matrix variants, `cases`, `aligned`, text/math styles, +accents, and spacing. Unknown commands or environments fail closed. + +**Hard rule — repair LaTeX upstream**: Unsupported source or an invalid marker +blocks the page. Rewrite within the supported subset without changing the +planned mathematics; otherwise return it to the content owner. Never substitute +a PNG, flatten structural math into ordinary text, hand-write OMML, or leave raw +LaTeX visible. + +**Compatibility boundary**: Both forms target Microsoft PowerPoint 2010+ Office +Math. WPS, Keynote, LibreOffice, and other clients receive no embedded formula +fallback and are outside the rendering/editability contract. + +**Validation**: The first-page/final SVG checker validates every marker and +compiles its LaTeX before release; native export repeats validation. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-hyperlinks.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-hyperlinks.md new file mode 100644 index 00000000..a43a935e --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-hyperlinks.md @@ -0,0 +1,100 @@ +# Native Hyperlink Specification + +Shared authoring contract for PowerPoint-native click hyperlinks on complete +objects and inline text runs. + +## 1. Trigger and Ownership + +**Trigger**: A user instruction, source fact, or page plan requires an external +destination or a jump to another slide in the same deck. + +| Layer | Ownership | +|---|---| +| Default Strategist | Record the linked text/object intent and exact target in the applicable §IX page block; never invent or normalize an unknown destination | +| Default Executor | Choose the whole-object or inline carrier and author the canonical SVG anchor | +| Active Quick context | Perform both content and authoring responsibilities directly | +| SVG-to-PPTX exporter | Validate the target, create the native relationship, and attach the click action | + +**Hard rule — page content only**: Hyperlinks are not a confirmation field, +resource, manifest, or `spec_lock.md` entry. Missing or ambiguous targets return +upstream; do not substitute a search result or guessed URL. + +--- + +## 2. Canonical SVG + +| Intent | Canonical form | +|---|---| +| Whole object, image, button, or group | `...` | +| Inline text | `Read the guide.` | +| Same-deck jump | `href="#slide-3"` using the 1-based final slide roster | +| Imported shape-plus-run conflict | Importer-only `data-pptx-shape-hyperlink="..."` on the logical ``, with standard inline anchors retained inside | + +**Hard rule — one target syntax**: Author SVG 2 `href`. Import may read legacy +`xlink:href`, but generated SVG never writes both. Same-deck destinations use +the exact `#slide-N` form and must resolve inside the final roster. External +destinations are absolute URIs with an explicit scheme; percent-encode spaces. +Relative paths, arbitrary fragments, filesystem paths, and `data:`, `file:`, +`javascript:`, or `vbscript:` destinations fail closed. + +**Hard rule — inline run**: Put visible text in one or more `` children +inside the anchor. The anchor and its descendants own no `x`, `y`, `dx`, or +`dy`; line positioning belongs to the enclosing line ``. A linked inline +formula uses one leaf formula `` inside the anchor and retains its native +math contract. + +**Hard rule — whole-object hit area**: Wrap at least one visible SVG element; +do not put direct text or a bare `` in a shape anchor. A multi-object +anchor links each exported leaf object. Include an explicit background shape +when gaps inside a button or card must also be clickable. + +Ordinary entrance, emphasis, motion-path, exit, and Morph animation may target +an outer top-level ``. A hyperlink-bearing group cannot also serve as an +interactive `trigger_shape`; use a separate trigger so one click has one owner. + +**Forbidden — ambiguous ownership**: Do not nest `` elements or place an +anchor inside `defs`, metadata, geometry-detail, or a native-replacement +subtree. A complete block formula or native Chart/Table marker may be wrapped +as one whole object; its preview descendants may not contain another anchor. + +**Forbidden — authored transport metadata**: Never author +`data-pptx-shape-hyperlink`. PPTX import uses it only when one source shape has +both a whole-shape click and descendant run links, because standard SVG cannot +nest their two anchors. Checker/export accept it only on that logical group +with at least one real inline `` descendant, then restore both native click +levels. Every ordinary whole-object link uses the standard outer ``. + +--- + +## 3. Native Result and Preservation + +| Carrier / target | Native result | +|---|---| +| Inline external link | `a:rPr/a:hlinkClick` plus an external hyperlink relationship | +| Whole-object external link | `p:cNvPr/a:hlinkClick` on each clickable leaf plus one shared external relationship | +| Inline or whole-object slide jump | The same click carrier plus an internal slide relationship and `ppaction://hlinksldjump` | +| Supported PPTX import | Reconstruct the same canonical SVG `` form | + +**Hard rule — Fill Native preservation**: Preserve external links. Retarget a +same-deck jump only when its source target maps unambiguously to one output +slide; omitted or duplicated targets fail closed instead of linking to an +orphan or wrong slide. + +**Hard rule — Enhance Native preservation**: Preserve existing hyperlink XML +and relationships unchanged. This route does not use the SVG authoring contract +to add new links. + +--- + +## 4. Exclusions and Validation + +**Forbidden — unsupported action settings**: Mouse-over links, custom shows, +first/last/next/previous navigation actions, program or macro execution, OLE or +file actions, and arbitrary `ppaction://` or relationship injection are outside +this contract. An `actionButton*` preset remains visual geometry until wrapped +in an ordinary supported hyperlink anchor. + +**Validation**: The final SVG checker validates carrier structure, target +syntax, and slide range. Export validates relationship type/mode and final +presentation-roster membership. Unsupported PPTX click actions produce an +import diagnostic; strict import fails rather than fabricating an SVG link. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-shape-authoring.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-shape-authoring.md index e61d9461..0e95c69f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-shape-authoring.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/native-shape-authoring.md @@ -55,7 +55,7 @@ paths or contours, or upgrade ordinary SVG during export. | Stock bent / curved relationship contour | `bentConnector*`, `curvedConnector*` | Prefer when the contour fits and endpoint attachment is not required. The authored object is an unconnected native Connector, so moving nodes does not reroute it. | | Stock callout | `wedgeRectCallout`, `wedgeRoundRectCallout`, `wedgeEllipseCallout`, `cloudCallout` | For a brand-specific or custom tail, continue through the Boolean gate; use freeform only if the result still cannot be expressed faithfully. | | Stock ribbon or scroll | `ribbon*`, `ellipseRibbon*`, `verticalScroll`, `horizontalScroll` | Select only when the stock contour is visually acceptable. | -| Standalone math symbol | `mathPlus`, `mathMinus`, `mathMultiply`, `mathDivide`, `mathEqual`, `mathNotEqual` | Inline formulas and prose symbols remain text/formula assets. | +| Standalone math symbol | `mathPlus`, `mathMinus`, `mathMultiply`, `mathDivide`, `mathEqual`, `mathNotEqual` | Use only when the symbol itself is a diagram shape; simple notation remains text, while non-trivial inline or block mathematics follows [`native-formula.md`](./native-formula.md). | | Literal Office symbol | `heart`, `sun`, `moon`, `lightningBolt`, `gear6`, `gear9` | Never replace an icon required by `spec_lock.icons`. | Use registry search for a less common literal shape: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md index 83cb7327..25b402dc 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards-core.md @@ -472,7 +472,8 @@ as a grouped editable text box. Authoring v1 creates only unconnected `p:cxnSp`; it does not accept hand-written endpoint/site metadata. An `actionButton*` preset maps visual geometry only. Preset appearance never invents connector attachment, action behavior, navigation targets, or -hyperlinks. +hyperlinks. Link and navigation behavior is authored explicitly instead — see +[`native-hyperlinks.md`](./native-hyperlinks.md). --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md index 8e6edcfe..6bf6fb63 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md @@ -2,9 +2,9 @@ # Strategist Image Planning -Always-on Stage-2 rendering-candidate extension plus conditional formula assets, confirmed image elaboration, and `design_spec.md §VIII` resource planning. +Always-on Stage-2 rendering-candidate extension plus confirmed image elaboration and `design_spec.md §VIII` resource planning. -**Trigger**: Load before every fresh Stage-2 direction set. Apply §2 first from the rendering index, freeze each direction's exact bases, and only then read the deduplicated selected detail files before completing its behavior. [`strategist.md`](./strategist.md) independently owns source recommendation. A confirmed non-`none` source activates the applicable resource-planning sections; confirmed `none` without formulas stops before resources, while a formula-only path applies §3 and §4 formula rows only. Rendering candidates are authored once before confirmation and never backfilled from a later source toggle. +**Trigger**: Load before every fresh Stage-2 direction set. Apply §2 first from the rendering index, freeze each direction's exact bases, and only then read the deduplicated selected detail files before completing its behavior. [`strategist.md`](./strategist.md) independently owns source recommendation. A confirmed non-`none` source activates the applicable resource-planning sections; confirmed `none` stops before resources. Rendering candidates are authored once before confirmation and never backfilled from a later source toggle. --- @@ -32,41 +32,20 @@ The UI hides these candidates while AI is not selected. If the user adds AI, it For specialized or regulated paper-figure subjects, preserve the prompt depth required by [`image-generator.md`](./image-generator.md) §4.2 rather than shortening to a generic brief. Scan the outline for genuine image-led pages, list the proposed hero pages in Stage-2 `image_notes` so the user can retain, edit, or remove them in the same confirmation, then mark only the confirmed pages' AI rows `page_role: hero_page`; local is the default. `text_policy: embedded` is reserved for stable figure-internal identifiers or lettering deliberately fused into the artwork; page titles, editable data values/labels, and prose remain SVG. Resolve confirmed provided assets through the context-first boundary above before writing §VIII. -## 3. Formula Asset Policy +## 3. Image Resource List -Formula rendering is a conditional choice surfaced in final Stage 2 production confirmation. Recommend one policy and let the user confirm or override it: - -| Policy | Behavior | Use | -|---|---|---| -| `mixed` (default) | Render complex expressions to PNG; keep simple inline math as editable text / Unicode | Most academic, engineering, educational, and technical decks | -| `render-all` | Render every formula-worthy expression to PNG | Formula-heavy teaching / research decks where consistency matters more than editability | -| `text-only` | Keep expressions as editable text / Unicode | Business decks, light technical briefs, or an explicit editability preference | - -`$...$` / `$$...$$` in source material are input signals only. Never scan output files for dollar-delimited formulas. Fractions, radicals, integrals, sums, limits, matrices, multiline derivations, and complex super/subscripts are formula-worthy; short variables, simple assignments, percentages, and expressions such as `O(n log n)` normally remain text. Never invent an equation for decoration. - -For `mixed` or `render-all`, write selected source expressions to `/images/formula_manifest.json` before writing the final spec, then run: - -```bash -python3 skills/ppt-master/scripts/latex_render.py -python3 skills/ppt-master/scripts/analyze_images.py /images -``` - -Follow `latex_render.py --help` for the manifest fields. The renderer writes dimensions, ratio, file, provider, and status back into it. Formula PNGs default to transparent; use an opaque final background only when the asset requires it. - -## 4. Image Resource List - -Add §VIII rows only for planned images and every selected formula; permitted unused sources create no row. Fill filename, dimensions/ratio, layout suggestion, crop, purpose/type, acquisition, status, reference, and conditional AI fields. `Acquire Via` is `ai`, `web`, `user`, `formula`, `placeholder`, or `slice`; status follows [`svg-image-embedding.md`](./svg-image-embedding.md). Keep any unavailable planned/required asset `Pending` or `Needs-Manual`; never delete or reclassify it to appear complete. After final confirmation, project each placed row into `spec_lock.md images` as ` | source= | pattern= | crop=` and omit unplaced source/sheet rows. Preserve exact confirmed `source`/`crop`; keep non-empty `pattern`, including optional catalog ids, as preferred expression rather than locked geometry. +Add §VIII rows only for planned images; permitted unused sources create no row. Fill filename, dimensions/ratio, layout suggestion, crop, purpose/type, acquisition, status, reference, and conditional AI fields. `Acquire Via` is `ai`, `web`, `user`, `placeholder`, or `slice`; status follows [`svg-image-embedding.md`](./svg-image-embedding.md). Keep any unavailable planned/required asset `Pending` or `Needs-Manual`; never delete or reclassify it to appear complete. After final confirmation, project each placed row into `spec_lock.md images` as ` | source= | pattern= | crop=` and omit unplaced source/sheet rows. Preserve exact confirmed `source`/`crop`; keep non-empty `pattern`, including optional catalog ids, as preferred expression rather than locked geometry. **Prepared derivatives**: Keep canonical; `Reference`: `Derived from ; treatment=;`. Deterministic child: distinct `.png`, inherits acquisition; §4.4 follows `user`/`ai` above. Lock placed children; [`image-base.md`](./image-base.md) §2–3 owns preparation. -References describe visual intent: AI uses subject + intent + composition without repeating rendering or HEX; web records exact subject, view/mood, focal/quiet region, and crop safety with positive quality cues; Image_Searcher later derives a separate short, specific provider query without rewriting this locked intent, while complete entity names or necessary disambiguation may use more words; formula preserves source LaTeX and placement intent. Any subject direction, focal placement, quiet region, or overlay-safety requirement that must affect acquisition/generation belongs in `Reference` or the matching §IX block, not only in `Layout pattern`. +References describe visual intent: AI uses subject + intent + composition without repeating rendering or HEX; web records exact subject, view/mood, focal/quiet region, and crop safety with positive quality cues; Image_Searcher later derives a separate short, specific provider query without rewriting this locked intent, while complete entity names or necessary disambiguation may use more words. Any subject direction, focal placement, quiet region, or overlay-safety requirement that must affect acquisition/generation belongs in `Reference` or the matching §IX block, not only in `Layout pattern`. -**Prepared-user fast path**: For initial imported or user-supplied assets confirmed as `provided`, copy the exact `Filename` basename and derive `Dimensions` / `Ratio` from that row's EXIF-corrected `Width` / `Height` / native `AspectRatio` in the latest `analysis/image_analysis.csv`; `SourceDisplayRatio` is source-context metadata, not the bitmap crop ratio. Drop source-side directories, set `Acquire Via: user` and `Status: Existing`, and decide the remaining §VIII fields normally. Existing §VIII / lock / provenance-manifest records override this inference. Assets declared as `ai`, `web`, `slice`, `formula`, or manual fulfillment retain that provenance and advance through their own status lifecycle after entering `images/`; location never reclassifies them as `user / Existing`. +**Prepared-user fast path**: For initial imported or user-supplied assets confirmed as `provided`, copy the exact `Filename` basename and derive `Dimensions` / `Ratio` from that row's EXIF-corrected `Width` / `Height` / native `AspectRatio` in the latest `analysis/image_analysis.csv`; `SourceDisplayRatio` is source-context metadata, not the bitmap crop ratio. Drop source-side directories, set `Acquire Via: user` and `Status: Existing`, and decide the remaining §VIII fields normally. Existing §VIII / lock / provenance-manifest records override this inference. Assets declared as `ai`, `web`, `slice`, or manual fulfillment retain that provenance and advance through their own status lifecycle after entering `images/`; location never reclassifies them as `user / Existing`. -**Mandatory**: each placed row, including formulas, gets one executable `Layout pattern`. It is preferred expression, not locked geometry; optional hierarchical ids from the already-read [`image-layout-patterns.md`](./image-layout-patterns.md) must be exact. They are prompt lookup handles for Executor, not exporter effect codes. Executor may adapt the suggestion while preserving resource identity/source, must-use status, crop/content, and explicit user/template constraints; layout-only changes need no upstream rewrite. +**Mandatory**: each placed row gets one executable `Layout pattern`. It is preferred expression, not locked geometry; optional hierarchical ids from the already-read [`image-layout-patterns.md`](./image-layout-patterns.md) must be exact. They are prompt lookup handles for Executor, not exporter effect codes. Executor may adapt the suggestion while preserving resource identity/source, must-use status, crop/content, and explicit user/template constraints; layout-only changes need no upstream rewrite. **Default — action-bearing image plan (may override when restraint better serves the page)**: For a `hero_page` or other image-led row, name an image/content or image/shape action—not position, size, crop, or legibility scrim alone. Plain split and full bleed remain valid when clearest. -Choose narrative intent before dimensions, then apply the already-read [`image-layout-spec.md`](./image-layout-spec.md) to the actual page region. Techniques needing a cutout, blurred crop, or desaturated copy require that prepared asset. Write `Crop Policy: no-crop` whenever cropping could remove required pixels, labels, evidence, identity, or edge content; screenshots, charts, certificates/contracts, dense diagrams, logos, product markings, and formulas are common triggers rather than an exhaustive list. Otherwise write `Crop Policy: adaptive`: Executor may use complete display or a focal-safe crop, and the value never commands cropping. Formula rows use `Type: Latex Formula`, `Acquire Via: formula`, `Crop Policy: no-crop`, and `Rendered` or `Needs-Manual`. +Choose narrative intent before dimensions, then apply the already-read [`image-layout-spec.md`](./image-layout-spec.md) to the actual page region. Techniques needing a cutout, blurred crop, or desaturated copy require that prepared asset. Write `Crop Policy: no-crop` whenever cropping could remove required pixels, labels, evidence, identity, or edge content; screenshots, charts, certificates/contracts, dense diagrams, logos, and product markings are common triggers rather than an exhaustive list. Otherwise write `Crop Policy: adaptive`: Executor may use complete display or a focal-safe crop, and the value never commands cropping. -Judge `text_policy` per AI row using [`image-generator.md`](./image-generator.md) §5.3; paper figures, academic schematics, panel comparisons, and data-axis graphics are positive triggers for reconsidering an all-`none` plan. Step 5 dispatches pending `ai` / `slice` rows to Image_Generator and pending `web` rows to Image_Searcher; formula rows bypass both. +Judge `text_policy` per AI row using [`image-generator.md`](./image-generator.md) §5.3; paper figures, academic schematics, panel comparisons, and data-axis graphics are positive triggers for reconsidering an all-`none` plan. Step 5 dispatches pending `ai` / `slice` rows to Image_Generator and pending `web` rows to Image_Searcher. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md index f0499ec9..552f62f8 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md @@ -34,7 +34,7 @@ solution + production gate: | Stage | Items | Role | |---|---|---| | **1 — communication contract + template choice** | `primary_language` · `c` audience · open-ended communication intent · audience outcome · core message / delivery context (primary + optional secondary) / artifact afterlife · `content_divergence` (all prose fields may be blank) · `a` canvas · explicit `free_design` or `templates` choice and selected roots | confirmed together; candidate workspaces do not influence the communication recommendation | -| **2 — final solution + production** (authored once from the user's *actual* Stage 1) | reading mode (`delivery_purpose`, PPT only) · `d` mode + visual style · `b` page count · `e` color · `f` icon · `g` typography · `h` image source + generated-image rendering · conditional natural-language template application · formula policy · conditional AI-image acquisition path · generation mode · refine-spec toggle · proactive speaker notes / custom animations / narration audio | derived as one coherent plan from the confirmed contract; internal template exporter modes remain hidden | +| **2 — final solution + production** (authored once from the user's *actual* Stage 1) | reading mode (`delivery_purpose`, PPT only) · `d` mode + visual style · `b` page count · `e` color · `f` icon · `g` typography · `h` image source + generated-image rendering · conditional natural-language template application · conditional AI-image acquisition path · generation mode · refine-spec toggle · proactive speaker notes / custom animations / narration audio | derived as one coherent plan from the confirmed contract; internal template exporter modes remain hidden | Do not force communication intent into one catalog label; Stage 1 records composite intent in prose. Editable prose fields are recommendation drafts, not required inputs: confirmation preserves current text and blanks; never repopulate a cleared field. Stage 2 confirms narrative spine, reading density, page budget, visual system, image direction, production mechanics, and how any installed template should be used. It never chooses or installs a template. Inspect only project-local template spec/prototypes, present one editable application plan, and keep exporter reuse/adherence internal. First author exactly three complete, project-fit solution directions from the confirmed contract and source; only then project each direction into mode, visual style, color, type, icons, and generated-image rendering for lower-level adjustment. Every direction projects a project-specific `custom` mode, `custom` visual style, and `custom` generated-image rendering; the fixed catalogs remain conservative lower-level single-select alternatives. All three must be viable and distinguishable as whole solutions, but do not force safe / shifted / bold archetypes, different catalog bases, or artificial extremes. After all three bundles are complete, compare them against the confirmed contract and source, choose the strongest overall fit, and write its actual zero-based index as `design_directions.selected` (`0`, `1`, or `2`); array order never determines preference. Every direction carries a complete generated-image rendering candidate even when AI imagery is not recommended; `recommend.image_usage` independently decides whether AI is proposed. Generated images inherit deck colors—there is no second image palette. Proactive defaults are speaker notes `true`, custom animations `false`, and narration audio `false`; a prior explicit user instruction overrides the matching recommendation, and effective narration audio requires effective speaker notes. Author each stage once; same-stage edits update only visible browser state through documented deterministic dependencies, without another AI/backend recommendation. Launch/derive/wait mechanics live in [`generate-pptx.md`](../workflows/generate-pptx.md) Step 4; item specs keep `a`–`h`. @@ -251,9 +251,27 @@ pinned, and changing canvas does not secretly rescale it. | Footnote / page number | 0.5–0.65× | Scan §IX before locking. Declare every recurring role, including `lead`, `footnote`, and chart annotations when used; a lead is always at least body size. Give each role one deck-wide anchor and snap derived anchors to clean even px (for body 24, a sound set is title 42, subtitle 32, lead 30, annotation 18, footnote 16). Executor may vary one occurrence within that role's anchor ±2px while preserving hierarchy and readability. A short non-structural Hero/Display size planned for at most two occurrences may remain undeclared; the third planned occurrence makes it recurring and requires an explicit named slot. Structural text never uses this sparse exception. -#### Formula Planning Trigger -Formula policy and formula-asset planning are conditional. [`strategist-image.md`](./strategist-image.md) is already loaded for Stage-2 rendering candidates; if the source contains formula-worthy expressions, or the user explicitly requests formula handling, apply its §3 before confirming the production policy or writing formula rows. Otherwise omit formula planning from the core path. +#### Mathematical Content Planning + +Preserve every source-backed equation and its mathematical meaning. In each +applicable §IX page block, record the exact expression under `Mathematical +content` as a LaTeX body without `$...$`, `$$...$$`, `\(...\)`, or `\[...\]` +source delimiters. This field may cover any mathematics that needs exact +preservation; do not classify it as inline or structural or choose its +implementation. Never invent an equation for decoration or create a formula +policy, manifest, PNG, §VIII row, or `spec_lock.md images` entry. Executor owns +the text-versus-native-formula decision and its authoring; if the supported +LaTeX subset cannot preserve the planned content, return here for a +content-level correction. + +#### Hyperlink Content Planning + +Preserve every explicit or source-backed link intent. In the applicable §IX +page block, record the linked text/object and its exact absolute URI or final +1-based same-deck slide target. Never guess an external destination, select the +inline/whole-object carrier, or create a link manifest or lock entry. Executor +owns SVG authoring under [`native-hyperlinks.md`](./native-hyperlinks.md). ### h. Image Source Recommendation @@ -274,9 +292,9 @@ Formula policy and formula-asset planning are conditional. [`strategist-image.md **Always-on decision module; conditional resource extension**: 1. Before authoring Stage-2 directions, read [`strategist-image.md`](./strategist-image.md) plus only [`image-renderings/_index.md`](./image-renderings/_index.md). After the three whole-direction intents exist and their rendering reference ids are frozen, read only those exact sibling files once and author one complete custom rendering inside each direction before deciding whether `recommend.image_usage` includes AI. -2. Independently derive `recommend.image_usage` from source needs. Confirmed non-`none` sources activate the module's resource-planning sections and the image layout references; formulas activate the formula sections even when usage is `none`. Confirmed `none` without formulas writes no image rows, but does not erase the three recommendation-only rendering candidates. +2. Independently derive `recommend.image_usage` from source needs. Confirmed non-`none` sources activate the module's resource-planning sections and the image layout references. Confirmed `none` writes no image rows, but does not erase the three recommendation-only rendering candidates. -The module owns formula policy, AI rendering alternatives, acquisition paths, resource rows, prompt depth, page roles, and placement intent. +The module owns AI rendering alternatives, acquisition paths, resource rows, prompt depth, page roles, and placement intent. ### Presentation Capability & Visualization Recall (Non-blocking — Strategist recommends, no user confirmation needed) @@ -484,7 +502,7 @@ final Stage 2 `false`, explicit objects-off, or explicit all-motion-off; only th includes transitions. 1. With Generate Step 4's retained complete final-confirmation state, read `${SKILL_DIR}/templates/design_spec_reference.md`. -2. Compose the whole Design Spec in active context before touching the target path. Create `design_spec.md` once from the schema marker through §X; do not copy a scaffold into the project or patch placeholder fields. Record production mechanics in §I, including one effective outcome plus provenance for Speaker Notes, Custom Animations, and Narration Audio. Resolve them from latest explicit user instruction → matching final Stage 2 proactive value → workflow default `enabled` / `disabled` / `disabled`; Narration Audio enabled requires Speaker Notes enabled without rewriting the raw proactive evidence, and a dependency-driven notes outcome records that provenance. In §IX, create the complete ordered roster; each entry carries layout, title, core message, **Audience move**, complete preferred wording, applicable capability recommendations, visualization/image references, sourced `Fact IDs`, and `Data class: scenario` for invented demo data. After Gate 1 plus conditional refine approval, roster ids/count/order and semantic content are authoritative; non-literal wording, block texture, layout, cover/closing composition, capability recommendations, and image/visualization patterns remain References unless promoted. +2. Compose the whole Design Spec in active context before touching the target path. Create `design_spec.md` once from the schema marker through §X; do not copy a scaffold into the project or patch placeholder fields. Record production mechanics in §I, including one effective outcome plus provenance for Speaker Notes, Custom Animations, and Narration Audio. Resolve them from latest explicit user instruction → matching final Stage 2 proactive value → workflow default `enabled` / `disabled` / `disabled`; Narration Audio enabled requires Speaker Notes enabled without rewriting the raw proactive evidence, and a dependency-driven notes outcome records that provenance. In §IX, create the complete ordered roster; each entry carries layout, title, core message, **Audience move**, complete preferred wording, exact mathematical content when applicable, capability recommendations, visualization/image references, sourced `Fact IDs`, and `Data class: scenario` for invented demo data. After Gate 1 plus conditional refine approval, roster ids/count/order and semantic content are authoritative; non-literal wording, block texture, layout, cover/closing composition, capability recommendations, and image/visualization patterns remain References unless promoted. 3. Compare `design_spec.md` against the final confirmation field by field. Repair every omission or deviation before entering an enabled refine-spec review or authoring `spec_lock.md`. 4. If enabled, run [`refine-spec`](../workflows/stages/refine-spec.md) after Gate 1; edit only that Design Spec and create no lock before explicit approval. 5. Read `${SKILL_DIR}/templates/spec_lock_reference.md`; create the lock once or resynchronize stale derived state from the approved Design Spec and context. Retain identity/refinements and stable roles/routing; omit unnamed page-local values, do not reopen evidence, and make no new recommendation. @@ -500,7 +518,7 @@ includes transitions. | Icons | §VI uses the confirmed library or confirmed no-icon/custom path | | Confirmed image-source set, `image_notes`, and AI strategy | §VIII uses only permitted sources and includes every explicitly required source, asset, or page role; a permitted but unused source needs no row | | Natural-language template application | §I records it and the relevant layout/prototype choices realize it without silently dropping a requested use or exclusion | -| Formula policy, AI-image acquisition path, generation mode, refine-spec toggle | §I records them as production mechanics; their owning Generate stage consumes the Design Spec, and formula policy also shapes §VIII when formula-worthy content exists | +| AI-image acquisition path, generation mode, refine-spec toggle | §I records them as production mechanics; their owning Generate stage consumes the Design Spec | | Proactive speaker notes, custom animations, and narration audio | §I records the three resolved effective outcomes with provenance, while §X records enabled note requirements or `Generation: disabled`; they remain outside `spec_lock.md`. §IX Motion suggestions remain optional advice regardless of the animation outcome | | Explicit final/literal narration script | §IX segments the argument by semantic scene and gives each segment a supporting visible state; §X records the source plus verbatim policy, and Generate freezes the actual segments in `notes/total.md` after Gate 2 | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md index f702454c..1ad5c2af 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md @@ -16,13 +16,12 @@ and filter/clip contracts. | Mode | Resource authority and preparation timing | |---|---| | Default Generate | `design_spec.md §VIII` plus its lock projection; when user-provided images are selected, run `analyze_images.py` after Strategist confirmation and complete the list before Executor | -| Quick Generate | Current main agent's active-context resource decisions; materialize explicit user paths first, resolve unspecified acquisition decisions automatically, and finish user/ai/web/slice/formula preparation before SVG authoring without confirmation or a persisted roster | +| Quick Generate | Current main agent's active-context resource decisions; materialize explicit user paths first, resolve unspecified acquisition decisions automatically, and finish user/ai/web/slice preparation before SVG authoring without confirmation or a persisted roster | ```markdown | Filename | Dimensions | Purpose | Type | Layout pattern | Crop Policy | Acquire Via | Status | Reference | |----------|------------|---------|------|----------------|-------------|-------------|--------|-----------| | team.jpg | 800x600 | Team photo | Photography | `#P1-02 image left, copy right` | adaptive | web | Pending | Diverse engineering team in modern office | -| formula_001.png | 736x168 | Page 3 block equation | Latex Formula | formula | no-crop | formula | Rendered | `E = mc^2` | ``` ### Image Status Enum @@ -33,8 +32,7 @@ and filter/clip contracts. | **Failed** | The latest automatic acquisition attempt failed; this is retryable and non-terminal | Step 5 reruns the owning manifest or explicitly resolves the row to `Needs-Manual`; Executor must never treat `Failed` as usable content | | **Generated** | AI/slice output exists | Reference from `../images/`; manifest records govern attribution. An `Illustration Sheet` stays in §VIII only as an unplaced slice source | | **Sourced** | Web-sourced file exists at expected path | Reference from `../images/`; check `image_sources.json` for `license_tier` — if `attribution-required`, render an inline credit element on the slide (see [`executor-web-image.md`](./executor-web-image.md) §1 and [`image-searcher.md`](./image-searcher.md) §7 for the attribution contract) | -| **Rendered** | Deterministic formula PNG exists at expected path (`Acquire Via: formula`) | Reference from `../images/`; use a legal anchor with `meet` for the complete placement (centered default: `xMidYMid meet`) and do not crop | -| **Needs-Manual** | Automatic acquisition is unavailable/exhausted or the selected path requires manual fulfillment; for `slice`, the parent sheet is unavailable | Default Generate may use a dashed placeholder until its readiness gate. Quick Generate blocks every required row still in this status, even if an unverified candidate file exists; validate a supplied replacement and reconcile it to `Existing`, `Generated`, `Sourced`, or `Rendered` first. For `slice`, supply the parent sheet and rerun `slice_images.py`; do not hand-place individual element files. | +| **Needs-Manual** | Automatic acquisition is unavailable/exhausted or the selected path requires manual fulfillment; for `slice`, the parent sheet is unavailable | Default Generate may use a dashed placeholder until its readiness gate. Quick Generate blocks every required row still in this status, even if an unverified candidate file exists; validate a supplied replacement and reconcile it to `Existing`, `Generated`, or `Sourced` first. For `slice`, supply the parent sheet and rerun `slice_images.py`; do not hand-place individual element files. | | **Existing** | User already has image (`Acquire Via: user`) | Place in `images/`, reference with `` | | **Placeholder** | Intentionally not prepared yet (`Acquire Via: placeholder`) | Dashed border placeholder; replace later | @@ -48,7 +46,6 @@ and filter/clip contracts. - Quick Generate → current main agent resolves the required resource in active context; explicit user paths/URLs/choices win, unspecified choices use automatic resolution, no interaction or persisted roster 2. Prepare project-local resources before SVG authoring: - user → materialize the explicit source under project/images/ → Existing - - formula → write formula_manifest.json and run latex_render.py → Rendered - Pending prepared derivative → follow [`image-base.md`](./image-base.md) §3 before ordinary `Acquire Via` dispatch - Pending / Failed + ai → Image_Generator runs image_gen.py → Generated - Pending / Failed + web → Image_Searcher runs image_search.py → Sourced @@ -58,7 +55,6 @@ and filter/clip contracts. ├── Sourced + license_tier=no-attribution → only ├── Sourced + license_tier=attribution-required → + small credit element on the slide ├── Sourced + license_tier=manual → only (user-supplied --from-url; rights/credit are user responsibility) - ├── Rendered formula → └── Placeholder / Needs-Manual → Dashed border + description text until a supplied file is validated and status is reconciled 4. Preview: python3 -m http.server -d 8000 → /svg_output/.svg 5. Export: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md index 253ca2aa..6a2e9c43 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md @@ -57,7 +57,7 @@ python3 scripts/update_repo.py | PPTX animations | `pptx_animations.py`, `animation_config.py` | [docs/pptx-animations.md](./docs/pptx-animations.md) | | Animation resources | `sound_sync.py` | [sound catalog](../templates/sounds/README.md); [docs/pptx-animations.md](./docs/pptx-animations.md) | | Spec maintenance | `update_spec.py`, `visualization_recall.py`; legacy `chart_recall.py` | [docs/update_spec.md](./docs/update_spec.md); [docs/visualization-recall.md](./docs/visualization-recall.md) | -| Image tools | `image_gen.py`, `image_treat.py`, `latex_render.py`, `analyze_images.py`, `gemini_watermark_remover.py` | [docs/image.md](./docs/image.md) | +| Image tools | `image_gen.py`, `image_treat.py`, `analyze_images.py`, `gemini_watermark_remover.py` | [docs/image.md](./docs/image.md) | | Maintenance smokes | Inline temporary-project commands | [advanced image and motion](./docs/advanced-image-motion-smoke.md); [mask and gradient](./docs/mask-gradient-smoke.md); [multilingual text](./docs/multilingual-text-smoke.md) | | Repo maintenance | `update_repo.py` | README install/update section | | Troubleshooting | validation, preview, export, dependency issues | [docs/troubleshooting.md](./docs/troubleshooting.md) | @@ -303,13 +303,16 @@ literal field fallback because they are shared by multiple slides. Image generation: ```bash -python3 scripts/latex_render.py -python3 scripts/latex_render.py --providers codecogs,quicklatex,mathpad,wikimedia python3 scripts/image_gen.py "A modern futuristic workspace" python3 scripts/image_gen.py --list-backends python3 scripts/analyze_images.py /images ``` +Generated-deck formulas do not use an image command. Author a native formula +marker in the page SVG; `svg_to_pptx.py` compiles its LaTeX metadata to editable +PowerPoint OMML. The retained `latex_render.py` utility is standalone legacy +rasterization only and is not connected to either Generate profile. + Repository update: ```bash diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py index 0a250053..1732ffe6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py @@ -1286,13 +1286,12 @@ def _stage2_production_recommendations_error( recommend = recommendations.get('recommend') if not isinstance(recommend, dict): recommend = {} - for field in ('formula_policy', 'generation_mode'): - value = recommend.get(field) - if not isinstance(value, str) or not value.strip(): - return ( - 'Stage 2 recommendations must include non-empty ' - f'recommend.{field}' - ) + generation_mode = recommend.get('generation_mode') + if not isinstance(generation_mode, str) or not generation_mode.strip(): + return ( + 'Stage 2 recommendations must include non-empty ' + 'recommend.generation_mode' + ) refine_spec = recommendations.get('refine_spec') if ( not isinstance(refine_spec, dict) @@ -1311,10 +1310,9 @@ def _stage2_production_recommendations_error( def _stage2_production_result_error(result: dict) -> Optional[str]: """Require every user-confirmed production control in the final payload.""" - for field in ('formula_policy', 'generation_mode'): - value = result.get(field) - if not isinstance(value, str) or not value.strip(): - return f'final Stage 2 payload must include non-empty {field}' + generation_mode = result.get('generation_mode') + if not isinstance(generation_mode, str) or not generation_mode.strip(): + return 'final Stage 2 payload must include non-empty generation_mode' if not isinstance(result.get('refine_spec'), bool): return 'final Stage 2 payload must include refine_spec as a boolean' if _uses_ai_images(result): @@ -2765,6 +2763,10 @@ def create_app( result_file, carry_previous=rec_stage_number > 1, ) + # Formula realization is Executor-owned. Accept the retired field from + # older recommendations/clients, but never persist it in a new receipt. + result.pop('formula_policy', None) + locked_values.pop('formula_policy', None) if rec_stage_number == 1 or not template_required: result.pop('template_application', None) locked_values.pop('template_application', None) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js index 190086f8..23b3519e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js @@ -104,7 +104,6 @@ placeholder_audience: "Who is this deck for?", placeholder_pages: "e.g. 12-15", hex_override: "Custom HEX override:", - formula_policy: "Formula rendering policy", image_ai_path: "AI image source", image_strategy: "Generated image style", image_strategy_empty: "No preset style references are available. You can still use a custom style.", @@ -287,7 +286,6 @@ placeholder_audience: "この資料は誰に向けたもの?", placeholder_pages: "例:12-15", hex_override: "カスタムHEXで上書き:", - formula_policy: "数式レンダリング方針", image_ai_path: "AI画像の生成元", image_strategy: "生成画像のスタイル", image_strategy_empty: "プリセットのスタイル見本を利用できません。カスタムスタイルは引き続き使用できます。", @@ -470,7 +468,6 @@ placeholder_audience: "这份演示文稿面向谁?", placeholder_pages: "如:12-15", hex_override: "自定义色值覆盖:", - formula_policy: "公式渲染策略", image_ai_path: "生成配图来源", image_strategy: "生成图风格", image_strategy_empty: "当前没有可用的预设风格参考,仍可使用自定义风格。", @@ -653,7 +650,6 @@ placeholder_audience: "這份簡報面向誰?", placeholder_pages: "如:12-15", hex_override: "自訂色值覆蓋:", - formula_policy: "公式渲染策略", image_ai_path: "生成配圖來源", image_strategy: "生成圖風格", image_strategy_empty: "目前沒有可用的預設風格參考,仍可使用自訂風格。", @@ -1350,7 +1346,6 @@ if (field === "icons") return REC.icons && REC.icons.value; if (field === "image_usage") return REC.images && REC.images.value; if (field === "image_ai_path") return REC.image_ai_path || (REC.images && REC.images.ai_path); - if (field === "formula_policy") return REC.typography && REC.typography.formula_policy && REC.typography.formula_policy.value; if (field === "generation_mode") return REC.generation_mode && REC.generation_mode.value; return REC[field] && REC[field].value; } @@ -3344,13 +3339,6 @@ } } - function renderFormulaPolicy(host) { - var sec = section("F", "formula_policy"); - enumField(sec, CAT.formula_policy, recOrFirst("formula_policy", CAT.formula_policy), - function () { return STATE.formula_policy; }, function (v) { STATE.formula_policy = v; }); - host.appendChild(sec); - } - // Combined color + typography + icon preview — not a separate confirmation, just a // live "overall impression" of the style choices made above. Kept // deliberately abstract (a style chip, not a slide layout); page layout @@ -3922,7 +3910,6 @@ host.appendChild(styleGroup); renderImageDirection(host); renderImageProduction(host); - renderFormulaPolicy(host); renderProactiveExecution(host); renderMode(host); renderRefine(host); @@ -4079,7 +4066,6 @@ } function initProductionState() { - STATE.formula_policy = pick("formula_policy", CAT.formula_policy); STATE.image_ai_path = pick("image_ai_path", CAT.image_ai_path); STATE.proactive_speaker_notes = booleanRecommendation("proactive_speaker_notes", true); STATE.proactive_custom_animations = booleanRecommendation("proactive_custom_animations", false); diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json index a6b7b514..47abd322 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json @@ -1483,44 +1483,6 @@ "desc_ja": "images/image_prompts.jsonにプロンプトを書き出し、ユーザーが外部で生成した画像をproject/images/に配置する方式。" } ], - "formula_policy": [ - { - "id": "mixed", - "label": "mixed", - "label_zh": "混合", - "label_zh_tw": "混合", - "label_en": "mixed", - "label_ja": "混合", - "desc_zh": "复杂公式渲染为图片;简单行内公式保持为可编辑文本或字符。", - "desc_zh_tw": "複雜公式渲染為圖片;簡單行內公式保持為可編輯文字或字元。", - "desc_en": "Render complex formula-worthy expressions to PNG; keep simple inline math as editable text / Unicode.", - "desc_ja": "複雑な数式はPNG画像として描画し、簡単な行内数式は編集可能なテキスト/Unicode文字のまま残す。" - }, - { - "id": "render-all", - "label": "render-all", - "label_zh": "全部渲染", - "label_zh_tw": "全部渲染", - "label_en": "render-all", - "label_ja": "すべて画像化", - "desc_zh": "所有需要按公式处理的表达式都渲染为图片。", - "desc_zh_tw": "所有需要按公式處理的運算式都渲染為圖片。", - "desc_en": "Render every formula-worthy expression to PNG.", - "desc_ja": "数式として扱うべき表現はすべてPNG画像として描画する。" - }, - { - "id": "text-only", - "label": "text-only", - "label_zh": "仅文本", - "label_zh_tw": "僅文字", - "label_en": "text-only", - "label_ja": "テキストのみ", - "desc_zh": "不渲染公式;表达式保持为可编辑文本或字符。", - "desc_zh_tw": "不渲染公式;運算式保持為可編輯文字或字元。", - "desc_en": "Do not render formulas; keep expressions as editable text / Unicode.", - "desc_ja": "数式は画像化せず、編集可能なテキスト/Unicode文字のまま保持する。" - } - ], "generation_mode": [ { "id": "continuous", diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md index a25af9d0..0393e8c1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md @@ -310,7 +310,7 @@ template-selection receipt. - **Enumerable + custom** — canvas / icons retain blank manual inputs. Mode and visual style first show three project-specific `custom` values projected from the complete directions, then the full fixed base catalog as conservative single-select alternatives. Selecting a projected card expands its behavior editor in place; edits change only the current value, while the adjusted active whole-direction card exposes an explicit restore action for the authored text. - **Visual examples for hard-to-name choices** — the full-screen confirmation page loads real SVG page samples from `static/style_previews/` for fixed `visual_style` catalog choices, and renders real sample SVGs from `templates/icons` for `icons`. Project-specific `custom` direction cards show their authored summary instead of requesting a nonexistent preset asset. These thumbnails and summaries make style and icon-library choices visually comparable before the user locks them. Preview copy is fixed role text (big title / section title / body / points), not project content from recommendation files, so users compare visual treatment rather than copywriting. These previews are a confirmation aid only: they do not add fields to recommendation stage files or `result.json`, and they do not replace the later Step 6 live preview. - **Image usage multi-select** — image sources are selected as one or more catalog ids: `ai` = AI-generated, `web` = Web-sourced, `provided` = User-provided, `placeholder` = Placeholder, `none` = No images. `none` is exclusive. A confirmed non-`none` set is the allowed acquisition-source boundary, not a requirement to use every selected source; only explicit `image_notes` wording can require a source, asset, or page role. Recommendation and result values may be a legacy single string, but new files should use an array. When several sources are recommended, write the source ids to `recommend.image_usage` and write the actual usage strategy to `image_notes`, not a custom prose value. -- **Closed enumerable** — PPT reading mode (`delivery_purpose` compatibility key), formula policy / generation mode / refine spec, plus AI source only when image usage includes `ai`. These have no Custom box; out-of-catalog values snap back to the recommended option. +- **Closed enumerable** — PPT reading mode (`delivery_purpose` compatibility key), generation mode / refine spec, plus AI source only when image usage includes `ai`. These have no Custom box; out-of-catalog values snap back to the recommended option. - **Proactive execution booleans** — Final Stage 2 carries top-level `proactive_speaker_notes`, `proactive_custom_animations`, and `proactive_narration_audio` values. Defaults are `true`, `false`, and `false`, respectively. They control what the Agent does proactively only when the user has not explicitly instructed otherwise; the latest explicit user instruction always wins. These three values are raw confirmation evidence: the UI and server neither couple nor rewrite them, and every boolean combination is valid. When narration audio is enabled, Strategist later resolves the effective Speaker Notes outcome to enabled and records `Narration Audio dependency` as its Design Spec provenance. Disabling proactive custom animation does not suppress the Strategist's advisory motion recommendations. - **Open prose** — `audience`, `communication_intent`, `audience_outcome`, `core_message`, `delivery_context`, `artifact_afterlife`, `content_divergence`, and `page_count`. `communication_intent` may carry several purposes plus priority / sequence; common paths appear only as help text. `delivery_context` states one primary presenter-led / reader-led / hybrid / recorded-self-running context plus optional secondary use; a hybrid recommendation names which context leads. `content_divergence` is the source-treatment axis. `page_count` may be a range here; Strategist resolves the exact §IX roster, leaving Executor no pagination latitude. - **Coordinated generative directions** — `design_directions` carries exactly three complete candidates authored top-down from the project contract. Each has a unique stable id and bundles `custom` mode, `custom` visual style, color, typography, icon id, and `custom` generated-image rendering regardless of recommended image source. Its localized note is a compact, user-facing style summary. It may reuse localized display labels from `catalogs.visual_styles` when they describe the result concisely, but those labels are optional vocabulary rather than a selection constraint or required mapping. Otherwise it uses concise natural language and never forces the nearest label. The summary stays within one or two short sentences and does not expose catalog ids or reference mechanics. All three must be viable as whole solutions; they do not need different catalog bases or forced safe / shifted / bold archetypes. After completing all three bundles, Strategist compares them against the confirmed contract and source, then writes the strongest overall fit's zero-based index to `selected`; array position does not determine preference. That bundle becomes the initial default and applies its three custom projections coherently. The page can still render legacy top-level `color`, `typography`, and `image_strategy` candidates, but new staged recommendations use the coordinated bundle. @@ -325,7 +325,7 @@ Direction-local custom projections apply to mode, visual style, and generated-im ## Catalogs — `static/catalogs.json` (the finite option universe) -The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while four-language labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`, `delivery_purpose`. Each entry is `{ "id", "label", "label_zh", "label_zh_tw", "label_en", "label_ja", ... }`; descriptions use `desc_zh` / `desc_zh_tw` / `desc_en` / `desc_ja`, and `visual_styles` groups use `group_zh` / `group_zh_tw` / `group_en` / `group_ja`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must cover all four languages (zh / zh-TW / en / ja). English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Simplified Chinese, Traditional Chinese, and Japanese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_zh_tw", "group_en", "group_ja", "items": [...] }]`. For `canvas` you only need to maintain the four-language labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`. +The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while four-language labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `icons`, `image_usage`, `image_ai_path`, `generation_mode`, `delivery_purpose`. Each entry is `{ "id", "label", "label_zh", "label_zh_tw", "label_en", "label_ja", ... }`; descriptions use `desc_zh` / `desc_zh_tw` / `desc_en` / `desc_ja`, and `visual_styles` groups use `group_zh` / `group_zh_tw` / `group_en` / `group_ja`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must cover all four languages (zh / zh-TW / en / ja). English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `continuous`, etc.); Simplified Chinese, Traditional Chinese, and Japanese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_zh_tw", "group_en", "group_ja", "items": [...] }]`. For `canvas` you only need to maintain the four-language labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`. ## Round-trip data contract @@ -363,7 +363,7 @@ run remains inactive. An existing `result.json` outside the current `stage1` / | Recommendation file | Declared stage | Page renders | Button | On submit | |---|---|---|---|---| | `recommendations.stage1.json` + `template_options.json` | `"stage1"` | communication contract — content language; audience; open `communication_intent`; audience outcome; core message / primary delivery context + optional secondary use / artifact afterlife / `content_divergence` (all prose fields may be blank); canvas; free-design/template mode and conditional candidate selectors | **Confirm contract & template choice** | writes Stage-1 `result.json` plus `template_selection.json` in one submission; the page stays open and polls while the agent installs/completes the handoff | -| `recommendations.stage2.json` | `"stage2"` | complete deck solution and production — conditional natural-language template application, reading mode, mode, page count, visual direction, color, icons, typography, image usage/rendering, conditional AI acquisition path, formula policy, proactive notes/custom-animation/narration-audio toggles, generation mode, and Design Spec review toggle | **Confirm final plan** | writes `result.json` `{ stage: "final", status: "confirmed", }`, then shuts the page down | +| `recommendations.stage2.json` | `"stage2"` | complete deck solution and production — conditional natural-language template application, reading mode, mode, page count, visual direction, color, icons, typography, image usage/rendering, conditional AI acquisition path, proactive notes/custom-animation/narration-audio toggles, generation mode, and Design Spec review toggle | **Confirm final plan** | writes `result.json` `{ stage: "final", status: "confirmed", }`, then shuts the page down | In the UI branch, the AI authors Stage 1 without reading template candidates, then launches the combined page. In chat/delegated confirmation it authors the @@ -436,11 +436,14 @@ The common paths — inform / explain / persuade / decide / align / teach / repo After Stage 1 is confirmed, create `recommendations.stage2.json` with the complete solution; leave Stage 1 unchanged (the server folds confirmed communication fields back in when serving the page): **Stage-2 production contract**: the server rejects the recommendation file -unless `recommend.formula_policy`, `recommend.generation_mode`, and boolean -`refine_spec.value` are present; `recommend.image_ai_path` is additionally -required when `image_usage` includes `ai`. Final submission must retain the -corresponding direct values (`formula_policy`, `generation_mode`, boolean -`refine_spec`, and conditional `image_ai_path`) or confirmation is rejected. +unless `recommend.generation_mode` and boolean `refine_spec.value` are present; +`recommend.image_ai_path` is additionally required when `image_usage` includes +`ai`. Final submission must retain the corresponding direct values +(`generation_mode`, boolean `refine_spec`, and conditional `image_ai_path`) or +confirmation is rejected. Formula realization is Executor-owned and is not a +Stage-2 choice. Legacy recommendation/result objects may contain +`formula_policy`; the server tolerates the extra field but does not render or +persist it in a new receipt. ```json { @@ -453,7 +456,6 @@ corresponding direct values (`formula_policy`, `generation_mode`, boolean "image_strategy": "custom", "image_usage": ["ai", "provided"], "image_ai_path": "auto", - "formula_policy": "mixed", "generation_mode": "continuous" }, "page_count": { "value": "12-15" }, @@ -554,7 +556,6 @@ Template-mode-only Stage-2 fragment: "icons": "tabler-outline", "typography": { "name": "...", "heading": { "primary": "...", "english": "...", "css": "..." }, "body": { "primary": "...", "english": "...", "css": "..." }, "body_size": 24, "body_size_unit": "px", "sizes": { "title": 42, "subtitle": 32, "annotation": 18 } }, "delivery_purpose": "balanced", - "formula_policy": "mixed", "image_usage": ["ai", "provided"], "image_notes": "封面和章节页用 AI 主视觉;产品页优先用户素材,缺口页可用占位符。", "image_ai_path": "auto", diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md index 6e3f5e90..eaf1ad30 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md @@ -200,6 +200,7 @@ Behavior: - preserves every readable chart dimension or series and emits `[Chart data warning: ]` for missing caches/count mismatches; `[Chart data unavailable: ]` is reserved for charts with no readable points (and unsupported ChartEx), so XY data is never flattened into a misleading category table - transcribes SmartArt semantic nodes as hierarchical Markdown; unreadable diagram data emits an explicit placeholder and conversion warning - exports embedded pictures to a sibling `_files/` directory +- preserves supported run, table-cell, picture, and text-shape links as Markdown links, including `#slide-N` jumps - appends speaker notes when present - writes `.conversion_profile.json` after successful conversion @@ -258,6 +259,12 @@ relationship preference for asset identity; its existing missing-media gate remains strict rather than silently treating the raster preview as the template's canonical asset. +Supported `a:hlinkClick` on shape/picture `p:cNvPr` and text `a:rPr` becomes +the shared SVG `` form for absolute external URIs and final-roster +`#slide-N` jumps. A source shape that also has linked inner runs uses the +importer-only `data-pptx-shape-hyperlink` transport to avoid nested SVG anchors. +Unsupported click actions produce a diagnostic; strict import stops. + ### Import compatibility and recovery boundary Import is tolerant by default because the source deck is user-owned or comes diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md index 7162831d..eaf03788 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md @@ -5,43 +5,25 @@ > inputs while delivery writes self-contained SVG previews and native PPTX > media. -Image tools cover formula rendering, prompt-based AI generation, web image search, image inspection, and Gemini watermark removal. +Image tools cover prompt-based AI generation, web image search, image inspection, +and Gemini watermark removal. Native formula authoring belongs to the SVG +pipeline, not the image pipeline. -## `latex_render.py` +## Legacy standalone `latex_render.py` -Manifest-driven LaTeX formula renderer. Default Generate has Strategist write -`images/formula_manifest.json` after Typography confirmation; Quick Generate -has the current agent write the same resource manifest without confirmation. -This script renders only those declared formulas to transparent PNGs and writes -dimensions back into the manifest. +This retained standalone utility renders a user-authored +`images/formula_manifest.json` to PNG. Neither Default nor Quick Generate calls +it, and new projects do not create formula manifests or formula images. The +supported generated-deck path authors a native formula marker in SVG and lets +`svg_to_pptx.py` compile its LaTeX payload to editable PowerPoint OMML. ```bash python3 scripts/latex_render.py python3 scripts/latex_render.py --dry-run -python3 scripts/latex_render.py --providers codecogs,quicklatex,mathpad,wikimedia ``` -Manifest shape: - -```json -{ - "providers": ["codecogs", "quicklatex", "mathpad", "wikimedia"], - "items": [ - { - "id": "formula_001", - "latex": "E = mc^2", - "display": "block", - "color": "#1D1D1F", - "background": "#FFFFFF", - "transparent": true, - "dpi": 300, - "filename": "formula_001.png" - } - ] -} -``` - -Output files land directly under `project/images/`. Formula filenames should use a shared `formula_` prefix, e.g. `formula_001.png`. The default provider chain is `codecogs,quicklatex,mathpad,wikimedia`; each provider is tried automatically until one succeeds, and the winning provider is recorded back into the manifest. `--providers` or manifest-level `providers` may override the order, but all four are available as no-key fallbacks. Formula PNGs are transparent by default. `background` is the temporary render matte and local background-removal reference; set `transparent: false` only when an opaque final formula asset is intentional. The script does not scan `spec_lock.md` or source documents for `$...$`; formula selection belongs to the active resource owner. +Use it only for an explicitly requested external raster workflow. It is not a +compatibility fallback for Keynote, WPS, LibreOffice, or another client. ## `image_gen.py` diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md index 10ba0cf3..aab7c049 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md @@ -524,6 +524,21 @@ It aggregates: Convert project SVGs into PPTX. +Native formulas use the two markers owned by +[`native-formula.md`](../../references/native-formula.md). A standalone block +stores delimiter-free LaTeX in the JSON metadata of +`` and exports `m:oMathPara`. A leaf +`preview` inside ordinary text +exports `m:oMath` in the same DrawingML paragraph as its surrounding runs; it +inherits computed size and visible solid fill, then uses the project text +language and Cambria Math. +Matrices, multiline derivations, and other high-structure expressions remain +blocks. Formula replacement is always active, independent of +`--native-charts-and-tables`: export replaces only the registered SVG preview +and writes editable PowerPoint 2010+ Office Math. It emits no formula PNG, media +relationship, or compatibility fallback, and makes no rendering/editability +promise for Keynote, WPS, LibreOffice, or another non-PowerPoint client. + ```bash python3 scripts/svg_to_pptx.py # Explicit compact image export: @@ -563,8 +578,8 @@ explicit current structured contract remains blocking. Explicit direct generation may use the [`quick-generate`](../../workflows/profiles/quick-generate.md) profile after the current agent has converted/read sources, researched identified factual gaps, -and prepared the required images, icons, formulas, and resource manifests as -needed. That profile skips Strategist, Confirm UI, `design_spec.md`, and +prepared the required images, icons, and resource manifests as needed, and +retained any source LaTeX for direct native-marker authoring. That profile skips Strategist, Confirm UI, `design_spec.md`, and `spec_lock.md`; it does not skip the resources required by the authored pages. After the complete SVG roster exists, run its lockless final checker, then export: @@ -584,8 +599,8 @@ custom object animation, and narration start off in Quick and may be enabled when needed. The exporter refuses a missing, blocking, non-final, or stale Quick final report before PPTX creation. Default-path output retains the normal postflight report and `backup/` snapshot; explicit `-o` retains the ordinary -no-backup behavior. Existing source, analysis, image/icon/formula, and -resource-manifest artifacts remain untouched. +no-backup behavior. Existing source, analysis, image/icon, and resource-manifest +artifacts remain untouched; formula source stays inside its authored SVG marker. For generated-project narration, follow the [`generate-audio`](../../workflows/stages/generate-audio.md) stage. It owns voice diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/hyperlink_contract.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/hyperlink_contract.py new file mode 100644 index 00000000..07f67bc0 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/hyperlink_contract.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +""" +PPT Master - Hyperlink Contract + +Parse and validate the shared SVG hyperlink surface used by quality checks, +SVG-to-PPTX export, and PPTX-to-SVG round-trip conversion. + +Usage: + from hyperlink_contract import parse_hyperlink_target + +Examples: + parse_hyperlink_target("https://example.com") + parse_hyperlink_target("#slide-3", slide_count=8) + +Dependencies: + None (standard library only). +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from xml.etree import ElementTree as ET + + +SVG_NS = "http://www.w3.org/2000/svg" +XLINK_NS = "http://www.w3.org/1999/xlink" +HYPERLINK_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/" + "hyperlink" +) +SLIDE_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" +) +SLIDE_JUMP_ACTION = "ppaction://hlinksldjump" +SHAPE_HYPERLINK_ATTR = "data-pptx-shape-hyperlink" + +_SLIDE_TARGET_RE = re.compile(r"#slide-([1-9][0-9]*)") +_URI_SCHEME_RE = re.compile(r"([A-Za-z][A-Za-z0-9+.-]*):") +_FORBIDDEN_EXTERNAL_SCHEMES = frozenset({ + "data", + "file", + "javascript", + "vbscript", +}) +_NON_OUTPUT_ANCESTORS = frozenset({ + "defs", + "desc", + "metadata", + "style", + "symbol", + "title", +}) +_INLINE_CONTAINER_TAGS = frozenset({"text", "tspan"}) +_INLINE_CONTENT_TAGS = frozenset({"tspan"}) +_UNSUPPORTED_LINK_BEHAVIOR_ATTRIBUTES = frozenset({ + "download", + "hreflang", + "ping", + "referrerpolicy", + "rel", + "target", + "type", +}) + + +class HyperlinkContractError(ValueError): + """Raised when one hyperlink cannot be represented faithfully in PPTX.""" + + +@dataclass(frozen=True) +class HyperlinkTarget: + """One normalized hyperlink destination.""" + + raw: str + kind: str + slide_number: int | None = None + + +def _local_name(elem: ET.Element) -> str: + if not isinstance(elem.tag, str): + return str(elem.tag) + return elem.tag.rsplit("}", 1)[-1] + + +def parse_hyperlink_target( + raw: str, + *, + slide_count: int | None = None, +) -> HyperlinkTarget: + """Parse one canonical SVG hyperlink target. + + Same-deck jumps use ``#slide-N``. External destinations must be absolute + URIs with an explicit scheme. Local files, script/data URLs, relative + paths, arbitrary SVG fragments, whitespace, and control characters fail + closed instead of being silently dropped during export. + """ + if not isinstance(raw, str): + raise HyperlinkContractError("hyperlink target must be a string") + if not raw: + raise HyperlinkContractError("hyperlink target must not be empty") + if raw != raw.strip(): + raise HyperlinkContractError( + "hyperlink target must not contain leading or trailing whitespace" + ) + if any(ord(char) < 0x21 or ord(char) == 0x7F for char in raw): + raise HyperlinkContractError( + "hyperlink target must not contain whitespace or control characters; " + "percent-encode URI spaces" + ) + if "\\" in raw: + raise HyperlinkContractError( + "hyperlink target must use URI syntax, not a filesystem path" + ) + + slide_match = _SLIDE_TARGET_RE.fullmatch(raw) + if slide_match is not None: + slide_number = int(slide_match.group(1)) + if slide_count is not None and slide_number > slide_count: + raise HyperlinkContractError( + f"slide jump {raw!r} exceeds the {slide_count}-slide deck" + ) + return HyperlinkTarget( + raw=f"#slide-{slide_number}", + kind="slide", + slide_number=slide_number, + ) + + if raw.startswith("#"): + raise HyperlinkContractError( + "same-deck hyperlinks must use the exact #slide-N form" + ) + scheme_match = _URI_SCHEME_RE.match(raw) + if scheme_match is None: + raise HyperlinkContractError( + "external hyperlinks must be absolute URIs with an explicit scheme" + ) + scheme = scheme_match.group(1).lower() + if scheme in _FORBIDDEN_EXTERNAL_SCHEMES: + raise HyperlinkContractError( + f"external hyperlink scheme {scheme!r} is not allowed" + ) + return HyperlinkTarget(raw=raw, kind="external") + + +def svg_hyperlink_href(elem: ET.Element) -> str: + """Return the sole href value from one SVG ```` element.""" + if _local_name(elem) != "a" or elem.tag != f"{{{SVG_NS}}}a": + raise HyperlinkContractError("hyperlink carrier must be an SVG ") + href = elem.get("href") + xlink_href = elem.get(f"{{{XLINK_NS}}}href") + if href is not None and xlink_href is not None: + raise HyperlinkContractError( + "SVG must not declare both href and xlink:href" + ) + value = href if href is not None else xlink_href + if value is None: + raise HyperlinkContractError("SVG must declare href") + return value + + +def project_hyperlink_errors( + root: ET.Element, + *, + slide_count: int | None = None, +) -> list[str]: + """Return fail-closed diagnostics for every SVG hyperlink carrier.""" + errors: list[str] = [] + parent_by_id = { + id(child): parent + for parent in root.iter() + for child in list(parent) + } + anchors = [ + elem for elem in root.iter() + if _local_name(elem) == "a" + ] + for index, anchor in enumerate(anchors, 1): + label = anchor.get("id") or f"anchor {index}" + if anchor.tag != f"{{{SVG_NS}}}a": + errors.append(f"{label}: hyperlink carrier must be an SVG ") + continue + try: + href = svg_hyperlink_href(anchor) + parse_hyperlink_target(href, slide_count=slide_count) + except HyperlinkContractError as exc: + errors.append(f"{label}: {exc}") + + unsupported_attrs = sorted( + name + for name in anchor.attrib + if name.rsplit("}", 1)[-1].lower() + in _UNSUPPORTED_LINK_BEHAVIOR_ATTRIBUTES + ) + if unsupported_attrs: + errors.append( + f"{label}: unsupported link behavior attribute(s): " + + ", ".join(unsupported_attrs) + ) + + ancestors: list[ET.Element] = [] + current = parent_by_id.get(id(anchor)) + while current is not None: + ancestors.append(current) + current = parent_by_id.get(id(current)) + ancestor_tags = [_local_name(elem) for elem in ancestors] + if "a" in ancestor_tags: + errors.append(f"{label}: nested SVG elements are not supported") + hidden_ancestor = next( + (tag for tag in ancestor_tags if tag in _NON_OUTPUT_ANCESTORS), + None, + ) + if hidden_ancestor is not None: + errors.append( + f"{label}: hyperlink cannot appear inside non-output " + f"<{hidden_ancestor}> content" + ) + replacement_ancestor = next( + ( + elem + for elem in ancestors + if elem.get("data-pptx-replace-with") is not None + or elem.get("data-pptx-part") == "geometry-detail" + ), + None, + ) + if replacement_ancestor is not None: + errors.append( + f"{label}: hyperlink cannot appear inside content replaced " + "or skipped during native export" + ) + + inline = any(tag in _INLINE_CONTAINER_TAGS for tag in ancestor_tags) + children = [ + child for child in list(anchor) + if _local_name(child) not in _NON_OUTPUT_ANCESTORS + ] + if inline: + if any(_local_name(child) not in _INLINE_CONTENT_TAGS for child in children): + errors.append( + f"{label}: inline hyperlink content may contain only " + ) + positioned = [ + elem + for elem in anchor.iter() + if any(elem.get(name) is not None for name in ("x", "y", "dx", "dy")) + ] + if positioned: + errors.append( + f"{label}: inline hyperlink cannot own x/y/dx/dy; " + "put line positioning on an enclosing " + ) + direct_parent = parent_by_id.get(id(anchor)) + if ( + direct_parent is not None + and _local_name(direct_parent) == "text" + and ( + direct_parent.get("data-paragraph-line-height") is not None + or any( + _local_name(sibling) == "tspan" + and any( + sibling.get(name) is not None + for name in ("x", "y", "dx", "dy") + ) + for sibling in list(direct_parent) + ) + ) + ): + errors.append( + f"{label}: multi-line hyperlinks must be nested inside " + "the owning line " + ) + visible_text = "".join(anchor.itertext()) + if not visible_text.strip(): + errors.append(f"{label}: inline hyperlink must contain visible text") + else: + if anchor.text and anchor.text.strip(): + errors.append( + f"{label}: shape hyperlink cannot contain direct text; " + "wrap text in " + ) + visible_children = [ + child for child in children + if child.get("data-pptx-part") != "geometry-detail" + ] + if not visible_children: + errors.append( + f"{label}: shape hyperlink must wrap at least one visual element" + ) + if any(_local_name(child) == "tspan" for child in children): + errors.append( + f"{label}: shape hyperlink cannot contain a bare ; " + "wrap inline runs in " + ) + + for index, carrier in enumerate( + ( + elem + for elem in root.iter() + if elem.get(SHAPE_HYPERLINK_ATTR) is not None + ), + 1, + ): + label = carrier.get("id") or f"shape hyperlink transport {index}" + raw_target = carrier.get(SHAPE_HYPERLINK_ATTR) or "" + try: + parse_hyperlink_target(raw_target, slide_count=slide_count) + except HyperlinkContractError as exc: + errors.append(f"{label}: {exc}") + if _local_name(carrier) != "g": + errors.append( + f"{label}: {SHAPE_HYPERLINK_ATTR} is allowed only on " + ) + ancestors: list[ET.Element] = [] + current = parent_by_id.get(id(carrier)) + while current is not None: + ancestors.append(current) + current = parent_by_id.get(id(current)) + if any(_local_name(elem) == "a" for elem in ancestors): + errors.append( + f"{label}: {SHAPE_HYPERLINK_ATTR} cannot be nested in " + ) + inline_anchors = [ + elem + for elem in carrier.iter(f"{{{SVG_NS}}}a") + if any( + _local_name(ancestor) in _INLINE_CONTAINER_TAGS + for ancestor in _ancestors_of(elem, parent_by_id) + ) + ] + if not inline_anchors: + errors.append( + f"{label}: {SHAPE_HYPERLINK_ATTR} is reserved for PPTX " + "round-trip groups that also contain inline hyperlinks" + ) + return errors + + +def _ancestors_of( + elem: ET.Element, + parent_by_id: dict[int, ET.Element], +) -> list[ET.Element]: + """Return ancestors from nearest parent to the SVG root.""" + ancestors: list[ET.Element] = [] + current = parent_by_id.get(id(elem)) + while current is not None: + ancestors.append(current) + current = parent_by_id.get(id(current)) + return ancestors + + +def trigger_shape_hyperlink_errors( + root: ET.Element, + trigger_group_ids: set[str] | frozenset[str], +) -> list[str]: + """Reject navigation links on click-trigger animation groups.""" + if not trigger_group_ids: + return [] + parent_by_id = { + id(child): parent + for parent in root.iter() + for child in list(parent) + } + errors: list[str] = [] + for elem in root.iter(): + group_id = elem.get("id") + if _local_name(elem) != "g" or group_id not in trigger_group_ids: + continue + has_link = elem.get(SHAPE_HYPERLINK_ATTR) is not None or any( + _local_name(descendant) == "a" + or descendant.get(SHAPE_HYPERLINK_ATTR) is not None + for descendant in elem.iter() + ) + if not has_link: + has_link = any( + _local_name(ancestor) == "a" + or ancestor.get(SHAPE_HYPERLINK_ATTR) is not None + for ancestor in _ancestors_of(elem, parent_by_id) + ) + if has_link: + errors.append( + f"animation trigger group {group_id!r} cannot also carry a " + "hyperlink; use an ordinary animation or a separate trigger" + ) + return errors + + +__all__ = [ + "HYPERLINK_REL_TYPE", + "HyperlinkContractError", + "HyperlinkTarget", + "SHAPE_HYPERLINK_ATTR", + "SLIDE_JUMP_ACTION", + "SLIDE_REL_TYPE", + "parse_hyperlink_target", + "project_hyperlink_errors", + "svg_hyperlink_href", + "trigger_shape_hyperlink_errors", +] diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_opc_validation.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_opc_validation.py index 5d159ba9..1f2e5a86 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_opc_validation.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_opc_validation.py @@ -9,12 +9,27 @@ from pathlib import Path from urllib.parse import urlsplit from xml.etree import ElementTree as ET +from hyperlink_contract import ( + HYPERLINK_REL_TYPE, + SLIDE_JUMP_ACTION, + SLIDE_REL_TYPE, +) + PACKAGE_REL_NS = ( "http://schemas.openxmlformats.org/package/2006/relationships" ) _RELATIONSHIPS_TAG = f"{{{PACKAGE_REL_NS}}}Relationships" _RELATIONSHIP_TAG = f"{{{PACKAGE_REL_NS}}}Relationship" +_DRAWINGML_NS = ( + "http://schemas.openxmlformats.org/drawingml/2006/main" +) +_PRESENTATIONML_NS = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" +) +_OFFICE_REL_NS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +) _OPC_UNRESERVED = frozenset( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" ) @@ -127,6 +142,126 @@ def resolve_internal_opc_target( return canonical_opc_part_path(resolved) +def _relationships_path_for_part(part_path: Path) -> Path: + return part_path.parent / "_rels" / f"{part_path.name}.rels" + + +def _relationship_attrs_by_id(rels_path: Path) -> dict[str, dict[str, str]]: + if not rels_path.is_file(): + return {} + root = ET.parse(rels_path).getroot() + return { + elem.attrib["Id"]: dict(elem.attrib) + for elem in root.findall(_RELATIONSHIP_TAG) + if elem.attrib.get("Id") + } + + +def _presentation_slide_roster(extract_dir: Path) -> set[str]: + """Return canonical slide parts reachable from ``p:sldIdLst``.""" + presentation = extract_dir / "ppt" / "presentation.xml" + presentation_rels = _relationships_path_for_part(presentation) + if not presentation.is_file() or not presentation_rels.is_file(): + return set() + rels = _relationship_attrs_by_id(presentation_rels) + root = ET.parse(presentation).getroot() + roster: set[str] = set() + rels_rel = presentation_rels.relative_to(extract_dir).as_posix() + for slide_id in root.findall( + f"{{{_PRESENTATIONML_NS}}}sldIdLst/" + f"{{{_PRESENTATIONML_NS}}}sldId" + ): + relationship_id = slide_id.attrib.get(f"{{{_OFFICE_REL_NS}}}id") + relationship = rels.get(relationship_id or "") + if relationship is None or relationship.get("Type") != SLIDE_REL_TYPE: + continue + target = relationship.get("Target", "") + resolved = resolve_internal_opc_target(rels_rel, target) + if resolved is not None: + roster.add(resolved) + return roster + + +def verify_hyperlink_relationships(extract_dir: Path) -> list[str]: + """Return hyperlink/action mismatches and slide jumps outside the roster.""" + roster = _presentation_slide_roster(extract_dir) + problems: list[str] = [] + source_patterns = ( + "ppt/slides/slide*.xml", + "ppt/slideLayouts/slideLayout*.xml", + "ppt/slideMasters/slideMaster*.xml", + ) + for pattern in source_patterns: + for part_path in sorted(extract_dir.glob(pattern)): + rels_path = _relationships_path_for_part(part_path) + rels = _relationship_attrs_by_id(rels_path) + part_rel = part_path.relative_to(extract_dir).as_posix() + rels_rel = rels_path.relative_to(extract_dir).as_posix() + try: + root = ET.parse(part_path).getroot() + except ET.ParseError: + continue + for link in root.iter(f"{{{_DRAWINGML_NS}}}hlinkClick"): + action = (link.attrib.get("action") or "").strip() + if action == "ppaction://media": + continue + relationship_id = ( + link.attrib.get(f"{{{_OFFICE_REL_NS}}}id") or "" + ).strip() + if not relationship_id: + if action == SLIDE_JUMP_ACTION: + problems.append( + f"{part_rel} -> " + ) + continue + relationship = rels.get(relationship_id) + if relationship is None: + problems.append( + f"{part_rel} -> " + ) + continue + rel_type = relationship.get("Type", "") + target_mode = relationship.get("TargetMode", "") + target = relationship.get("Target", "") + if rel_type == HYPERLINK_REL_TYPE: + if target_mode.lower() != "external": + problems.append( + f"{part_rel} -> " + ) + if action == SLIDE_JUMP_ACTION: + problems.append( + f"{part_rel} -> " + ) + continue + if rel_type == SLIDE_REL_TYPE: + if target_mode: + problems.append( + f"{part_rel} -> " + ) + if action != SLIDE_JUMP_ACTION: + problems.append( + f"{part_rel} -> " + ) + resolved = resolve_internal_opc_target(rels_rel, target) + if resolved is not None and resolved not in roster: + problems.append( + f"{part_rel} -> " + ) + continue + if action == SLIDE_JUMP_ACTION: + problems.append( + f"{part_rel} -> " + ) + return problems + + def verify_internal_relationships(extract_dir: Path) -> list[str]: """Return invalid or dangling internal relationships in an OPC package.""" package_parts: set[str] = set() @@ -207,4 +342,5 @@ def verify_internal_relationships(extract_dir: Path) -> list[str]: ) elif resolved not in package_parts: problems.append(f"{rels_rel} -> {resolved}") + problems.extend(verify_hyperlink_relationships(extract_dir)) return problems diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/hyperlinks.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/hyperlinks.py new file mode 100644 index 00000000..5aae61a5 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/hyperlinks.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +PPT Master - PPTX Hyperlink Resolver + +Resolve native DrawingML click actions into the canonical SVG ``href`` form +used by the authoring and round-trip pipelines. + +Usage: + from .hyperlinks import resolve_click_hyperlink + +Examples: + result = resolve_click_hyperlink(rels, "rId3", "", slide_index_by_part=roster) + +Dependencies: + PPT Master hyperlink contract. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from hyperlink_contract import ( + HYPERLINK_REL_TYPE, + HyperlinkContractError, + SLIDE_JUMP_ACTION, + SLIDE_REL_TYPE, + parse_hyperlink_target, +) + + +@dataclass(frozen=True) +class HyperlinkResolution: + """Resolved SVG target or one actionable source-package diagnostic.""" + + href: str | None = None + error: str | None = None + + +def resolve_click_hyperlink( + relationships: dict[str, dict[str, str]], + relationship_id: str, + action: str, + *, + slide_index_by_part: dict[str, int], +) -> HyperlinkResolution: + """Resolve a click hyperlink relationship from one OOXML source part.""" + if action == "ppaction://media": + return HyperlinkResolution() + if not relationship_id: + return HyperlinkResolution(error="click hyperlink has no relationship id") + relationship = relationships.get(relationship_id) + if relationship is None: + return HyperlinkResolution( + error=f"click hyperlink relationship {relationship_id!r} is missing" + ) + rel_type = relationship.get("type", "") + target = relationship.get("target", "") + external = relationship.get("external") == "1" + + if action == SLIDE_JUMP_ACTION: + if external or rel_type != SLIDE_REL_TYPE: + return HyperlinkResolution( + error="hlinksldjump does not reference an internal slide" + ) + slide_index = slide_index_by_part.get(target) + if slide_index is None: + return HyperlinkResolution( + error=f"slide jump target {target!r} is not in the presentation roster" + ) + return HyperlinkResolution(href=f"#slide-{slide_index}") + + if action.startswith("ppaction://") and action not in { + "ppaction://hlinkurl", + }: + return HyperlinkResolution( + error=f"unsupported PowerPoint click action {action!r}" + ) + if not external or rel_type != HYPERLINK_REL_TYPE: + return HyperlinkResolution( + error="click hyperlink does not reference an external hyperlink" + ) + try: + parsed = parse_hyperlink_target(target) + except HyperlinkContractError as exc: + return HyperlinkResolution(error=str(exc)) + if parsed.kind != "external": + return HyperlinkResolution( + error="external hyperlink relationship resolved to a slide target" + ) + return HyperlinkResolution(href=parsed.raw) + + +__all__ = ["HyperlinkResolution", "resolve_click_hyperlink"] diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py index 47ae6cd4..9e8716c9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py @@ -424,6 +424,11 @@ class OoxmlPackage: return self._slides[index - 1] return None + @property + def slide_index_by_part(self) -> dict[str, int]: + """Return final presentation-order indices keyed by slide part path.""" + return {slide.part.path: slide.index for slide in self._slides} + def iter_all_masters(self) -> Iterator[PartRef]: """Yield every slideMaster declared in presentation.xml, regardless of whether any slide currently uses it. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py index 945e80bb..398b000e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py @@ -71,6 +71,8 @@ class ShapeNode: name: str = "" spid: str = "" hidden: bool = False + hyperlink_rid: str = "" + hyperlink_action: str = "" placeholder: PlaceholderInfo | None = None inherited_lst_styles: tuple[ET.Element, ...] = () inherited_body_properties: tuple[ET.Element, ...] = () @@ -85,7 +87,10 @@ class ShapeNode: # Walker # --------------------------------------------------------------------------- -def _read_nv_sp_pr(parent: ET.Element, nv_tag: str) -> tuple[str, str, bool, PlaceholderInfo | None]: +def _read_nv_sp_pr( + parent: ET.Element, + nv_tag: str, +) -> tuple[str, str, bool, PlaceholderInfo | None, str, str]: """Extract name/id/hidden/placeholder from an nvXXXPr container. nv_tag is one of nvSpPr / nvPicPr / nvCxnSpPr / nvGrpSpPr / nvGraphicFramePr. @@ -95,8 +100,10 @@ def _read_nv_sp_pr(parent: ET.Element, nv_tag: str) -> tuple[str, str, bool, Pla spid = "" hidden = False ph: PlaceholderInfo | None = None + hyperlink_rid = "" + hyperlink_action = "" if container is None: - return name, spid, hidden, ph + return name, spid, hidden, ph, hyperlink_rid, hyperlink_action cnv = container.find("p:cNvPr", NS) if cnv is not None: @@ -104,6 +111,10 @@ def _read_nv_sp_pr(parent: ET.Element, nv_tag: str) -> tuple[str, str, bool, Pla spid = cnv.attrib.get("id", "") if ooxml_bool(cnv.attrib.get("hidden")): hidden = True + hyperlink = cnv.find("a:hlinkClick", NS) + if hyperlink is not None: + hyperlink_rid = hyperlink.attrib.get(f"{{{NS['r']}}}id", "") + hyperlink_action = hyperlink.attrib.get("action", "") nv_pr = container.find("p:nvPr", NS) if nv_pr is not None: @@ -116,7 +127,7 @@ def _read_nv_sp_pr(parent: ET.Element, nv_tag: str) -> tuple[str, str, bool, Pla orient=ph_elem.attrib.get("orient"), ) - return name, spid, hidden, ph + return name, spid, hidden, ph, hyperlink_rid, hyperlink_action def _resolve_xfrm(shape: ET.Element, kind: str) -> ET.Element | None: @@ -248,7 +259,14 @@ def _walk_container( continue kind, nv_tag = kind_info - name, spid, hidden, ph = _read_nv_sp_pr(child, nv_tag) + ( + name, + spid, + hidden, + ph, + hyperlink_rid, + hyperlink_action, + ) = _read_nv_sp_pr(child, nv_tag) xfrm = parse_xfrm(_resolve_xfrm(child, kind)) effective_rotation = (ancestor_rotation + xfrm.rot) % 360.0 @@ -288,6 +306,8 @@ def _walk_container( node = ShapeNode( kind=kind, xml=child, xfrm=xfrm, name=name, spid=spid, hidden=hidden, placeholder=ph, + hyperlink_rid=hyperlink_rid, + hyperlink_action=hyperlink_action, inherited_lst_styles=inherited_lst_styles, inherited_body_properties=inherited_body_properties, effective_rotation=effective_rotation, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py index 8fb881e8..7517f771 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py @@ -42,6 +42,7 @@ from pptx_effects import ( txbody_has_run_effects, unsupported_effect_metadata, ) +from hyperlink_contract import SHAPE_HYPERLINK_ATTR from .color_resolver import ColorPalette, find_color_elem, resolve_color from .chart_to_svg import CHART_URI, CHARTEX_URI, extract_native_chart_payload @@ -57,6 +58,7 @@ from .import_diagnostics import ( ImportDiagnostic, append_diagnostic, ) +from .hyperlinks import resolve_click_hyperlink from .ln_to_svg import StrokeResult, resolve_stroke from .ooxml_loader import ( OoxmlPackage, @@ -169,6 +171,30 @@ def _diagnose_picture_result( ) +def _resolve_svg_hyperlink( + ctx: AssemblyContext, + relationship_id: str, + action: str, +) -> str | None: + """Resolve one source-part click link or record its explicit loss.""" + resolution = resolve_click_hyperlink( + ctx.slide_part.rels, + relationship_id, + action, + slide_index_by_part=ctx.pkg.slide_index_by_part, + ) + if resolution.error is None: + return resolution.href + if ctx.strict: + raise ValueError(resolution.error) + ctx.diagnose( + "hyperlink-omitted", + resolution.error, + "retain the object and omit only its unsupported click link", + ) + return None + + # --------------------------------------------------------------------------- # Public entry # --------------------------------------------------------------------------- @@ -532,6 +558,11 @@ def _convert_shape(node: ShapeNode, ctx: AssemblyContext, *, top_level: bool) -> fallback_lst_styles=node.inherited_lst_styles, id_prefix=f"{ctx.group_id_prefix}txt", id_seq=ctx.grad_seq, + hyperlink_resolver=lambda rid, action: _resolve_svg_hyperlink( + ctx, + rid, + action, + ), ) else: text_result = convert_txbody( @@ -543,6 +574,11 @@ def _convert_shape(node: ShapeNode, ctx: AssemblyContext, *, top_level: bool) -> fallback_lst_styles=node.inherited_lst_styles, id_prefix=f"{ctx.group_id_prefix}txt", id_seq=ctx.grad_seq, + hyperlink_resolver=lambda rid, action: _resolve_svg_hyperlink( + ctx, + rid, + action, + ), ) if tx_body is not None else TextResult() except ValueError as exc: if ctx.strict: @@ -1222,6 +1258,11 @@ def _render_graphic_table( id_prefix=f"tbl{ctx.shape_seq[0]}", grad_seq=ctx.grad_seq, marker_seq=ctx.marker_seq, + hyperlink_resolver=lambda rid, action: _resolve_svg_hyperlink( + ctx, + rid, + action, + ), ) if result.defs: ctx.defs.extend(result.defs) @@ -1573,7 +1614,21 @@ def _wrap_shape_group( ) if transform: attrs.append(f'transform="{transform}"') - return f"\n{inner}\n" + group_xml = f"\n{inner}\n" + if node.hyperlink_rid or node.hyperlink_action: + href = _resolve_svg_hyperlink( + ctx, + node.hyperlink_rid, + node.hyperlink_action, + ) + if href is not None and '\n{inner}\n" + if href is not None: + return f'{group_xml}' + return group_xml def _attrs_to_xml(attrs: dict[str, str]) -> str: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py index 1f1ce210..8cff3da6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py @@ -56,6 +56,7 @@ from .emu_units import ( from .fill_to_svg import FillResult, resolve_fill from .ln_to_svg import resolve_stroke from .txbody_to_svg import _resolve_theme_typeface, convert_txbody +from .txbody_to_svg import HyperlinkResolver BUILTIN_MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}" @@ -279,6 +280,7 @@ def convert_tbl( id_prefix: str = "tbl", grad_seq: list[int] | None = None, marker_seq: list[int] | None = None, + hyperlink_resolver: HyperlinkResolver | None = None, ) -> TableResult: """Render an at the given absolute xfrm into SVG markup.""" grad_seq = grad_seq if grad_seq is not None else [0] @@ -414,6 +416,7 @@ def convert_tbl( slide_number=slide_number, id_prefix=f"{id_prefix}txt", id_seq=grad_seq, + hyperlink_resolver=hyperlink_resolver, ) defs.extend(text_result.defs) if text_result.svg: @@ -1549,6 +1552,7 @@ def _convert_cell_text( slide_number: int | None, id_prefix: str, id_seq: list[int] | None, + hyperlink_resolver: HyperlinkResolver | None, ): """Render cell text. PowerPoint's can override txBody insets via its own marL/marR/marT/marB attrs; convert_txbody reads from , @@ -1602,6 +1606,7 @@ def _convert_cell_text( slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, + hyperlink_resolver=hyperlink_resolver, ) except (AttributeError, OverflowError, TypeError, ValueError): plain_tx_body = _plain_table_text_body(render_tx_body, overrides) @@ -1611,6 +1616,7 @@ def _convert_cell_text( slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, + hyperlink_resolver=hyperlink_resolver, ) finally: if overrides and body_pr is not None: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py index b3240f1e..1550a35d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py @@ -23,6 +23,7 @@ back to paragraph/list defaults, endParaRPr, or spec-default values. from __future__ import annotations from dataclasses import dataclass, field +from typing import Callable from xml.etree import ElementTree as ET from svg_to_pptx.drawingml.utils import detect_text_lang, is_cjk_char @@ -64,6 +65,10 @@ class TextRun: strikethrough: bool = False letter_spacing_px: float = 0.0 is_break: bool = False # marks an a:br within a paragraph + hyperlink_href: str | None = None + + +HyperlinkResolver = Callable[[str, str], str | None] @dataclass @@ -114,6 +119,7 @@ def convert_txbody( fallback_run_props: tuple[ET.Element, ...] = (), id_prefix: str = "txt", id_seq: list[int] | None = None, + hyperlink_resolver: HyperlinkResolver | None = None, ) -> TextResult: """Convert under the given shape geometry to SVG (s).""" if tx_body is None: @@ -126,6 +132,7 @@ def convert_txbody( fallback_lst_styles=fallback_lst_styles, fallback_run_props=fallback_run_props, slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, + hyperlink_resolver=hyperlink_resolver, ) if not paragraphs or not _has_visible_text(paragraphs): return TextResult() @@ -224,6 +231,7 @@ def convert_vertical_txbody( fallback_run_props: tuple[ET.Element, ...] = (), id_prefix: str = "txt", id_seq: list[int] | None = None, + hyperlink_resolver: HyperlinkResolver | None = None, ) -> TextResult: """Render East Asian vertical text as upright stacked glyphs. @@ -241,6 +249,7 @@ def convert_vertical_txbody( fallback_lst_styles=fallback_lst_styles, fallback_run_props=fallback_run_props, slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, + hyperlink_resolver=hyperlink_resolver, ) runs = [ run @@ -280,12 +289,15 @@ def convert_vertical_txbody( if first_run is None: first_run = run first_baseline = baseline_y - spans.append(f"{_xml_escape(char)}") + run_span = f"{_xml_escape(char)}" + spans.append(_wrap_run_hyperlink(run_span, run)) else: dy = baseline_y - (previous_baseline or baseline_y) + run_span = f"{_xml_escape(char)}" + linked_span = _wrap_run_hyperlink(run_span, run) spans.append( - f'{_xml_escape(char)}" + f'' + f"{linked_span}" ) previous_baseline = baseline_y cursor_y += advance @@ -374,6 +386,7 @@ def _parse_paragraphs( slide_number: int | None = None, id_prefix: str = "txt", id_seq: list[int] | None = None, + hyperlink_resolver: HyperlinkResolver | None = None, ) -> list[TextParagraph]: """Walk children producing TextParagraph objects.""" paragraphs: list[TextParagraph] = [] @@ -393,6 +406,7 @@ def _parse_paragraphs( default_font_size_px=default_font_size_px, slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, + hyperlink_resolver=hyperlink_resolver, ) paragraphs.append(para) @@ -412,6 +426,7 @@ def _parse_paragraph( slide_number: int | None = None, id_prefix: str = "txt", id_seq: list[int] | None = None, + hyperlink_resolver: HyperlinkResolver | None = None, ) -> TextParagraph: para = TextParagraph() @@ -452,6 +467,7 @@ def _parse_paragraph( default_fill=default_fill, default_font_size_px=default_font_size_px, id_prefix=id_prefix, id_seq=id_seq, + hyperlink_resolver=hyperlink_resolver, ) for child in list(p_elem): @@ -517,6 +533,7 @@ def _build_run( default_font_size_px: float = DEFAULT_FONT_SIZE_PX, id_prefix: str = "txt", id_seq: list[int] | None = None, + hyperlink_resolver: HyperlinkResolver | None = None, ) -> TextRun: """Resolve a single run from its rPr and fallback run properties.""" style_chain = ( @@ -605,6 +622,14 @@ def _build_run( ) font_family = _build_font_stack(latin_face, ea_face, cs_face) + hyperlink_href: str | None = None + if rpr is not None and hyperlink_resolver is not None: + hyperlink = rpr.find("a:hlinkClick", NS) + if hyperlink is not None: + hyperlink_href = hyperlink_resolver( + hyperlink.attrib.get(f"{{{NS['r']}}}id", ""), + hyperlink.attrib.get("action", ""), + ) return TextRun( text=text, @@ -618,6 +643,7 @@ def _build_run( underline=underline, strikethrough=strikethrough, letter_spacing_px=letter_spacing_px, + hyperlink_href=hyperlink_href, ) @@ -1125,6 +1151,7 @@ def _copy_run(run: TextRun, *, text: str) -> TextRun: underline=run.underline, strikethrough=run.strikethrough, letter_spacing_px=run.letter_spacing_px, + hyperlink_href=run.hyperlink_href, ) @@ -1238,6 +1265,23 @@ def _emit_paragraph( f'dy="{fmt_num(line_advance)}">' ) continue + line_has_hyperlink = any(run.hyperlink_href for run in line) + if line_has_hyperlink: + run_spans = ''.join( + _wrap_run_hyperlink( + f'' + f'{_xml_escape(run.text)}', + run, + ) + for run in line + ) + position_attrs = ( + f' x="{fmt_num(anchor_x)}" dy="{fmt_num(line_advance)}"' + if line_advance is not None + else '' + ) + spans.append(f'{run_spans}') + continue for run_idx, run in enumerate(line): attrs = _run_tspan_attrs(run) if run_idx == 0 and line_advance is not None: @@ -1318,6 +1362,13 @@ def _run_tspan_attrs(run: TextRun) -> str: return " " + " ".join(parts) +def _wrap_run_hyperlink(markup: str, run: TextRun) -> str: + """Wrap one visible SVG run in the canonical hyperlink carrier.""" + if not run.hyperlink_href: + return markup + return f'{markup}' + + def _xml_escape(text: str) -> str: return (text.replace("&", "&") .replace("<", "<") diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json index e20e55d4..74605c27 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json @@ -18,7 +18,7 @@ "file_budgets": { "AGENTS.md": 2750, "skills/ppt-master/SKILL.md": 1250, - "skills/ppt-master/references/executor-base.md": 9250, + "skills/ppt-master/references/executor-base.md": 10000, "skills/ppt-master/references/executor-structured.md": 5500, "skills/ppt-master/references/executor-chart.md": 3500, "skills/ppt-master/references/executor-visualization.md": 1250, @@ -326,15 +326,23 @@ "max_tokens": 110000 }, "route.generate.quick-generate.formula": { - "description": "Quick Generate with manifest-driven formula rendering and formula-image realization.", + "description": "Quick Generate with direct PowerPoint-native formula marker authoring.", "scope": "cumulative", "include": [ "route.generate.quick-generate", - "stage.generate.executor.image" + "stage.generate.executor.formula" ], - "files": [ - "skills/ppt-master/scripts/docs/image.md" + "files": [], + "max_tokens": 110000 + }, + "route.generate.quick-generate.hyperlink": { + "description": "Quick Generate with direct PowerPoint-native external and same-deck hyperlink authoring.", + "scope": "cumulative", + "include": [ + "route.generate.quick-generate", + "stage.generate.executor.hyperlink" ], + "files": [], "max_tokens": 110000 }, "route.generate.quick-generate.ai-two-types": { @@ -386,14 +394,13 @@ "max_tokens": 115000 }, "route.generate.planning-formula": { - "description": "Formula-only planning context, including the shared image/formula layout math and vocabulary.", + "description": "Formula-bearing planning context; Strategist records exact mathematics without loading image-layout or formula-rendering resources.", "scope": "cumulative", "include": [ - "route.generate.planning", - "stage.generate.strategist.image" + "route.generate.planning" ], "files": [], - "max_tokens": 115000 + "max_tokens": 110000 }, "route.generate.planning-ai": { "description": "Generate-PPTX planning after AI imagery is confirmed, adding image-layout and resource-planning authorities to the rendering index and already-selected rendering bases.", @@ -538,6 +545,30 @@ "files": [], "max_tokens": 165000 }, + "route.generate.flat-formula": { + "description": "Default flat Generate-PPTX context with direct PowerPoint-native formula marker authoring and no image branch.", + "scope": "cumulative", + "include": [ + "route.generate.planning-formula", + "stage.generate.executor.flat", + "stage.generate.executor.formula", + "stage.generate.executor.notes" + ], + "files": [], + "max_tokens": 170000 + }, + "route.generate.flat-hyperlink": { + "description": "Default flat Generate-PPTX context with PowerPoint-native external and same-deck hyperlink authoring.", + "scope": "cumulative", + "include": [ + "route.generate.planning", + "stage.generate.executor.flat", + "stage.generate.executor.hyperlink", + "stage.generate.executor.notes" + ], + "files": [], + "max_tokens": 170000 + }, "route.generate.flat-ai-two-types": { "description": "Generate-PPTX context with AI images, the selected preset or exact custom rendering bases, and two local types.", "scope": "cumulative", @@ -552,7 +583,7 @@ "max_tokens": 205000 }, "route.generate.flat-in-hand-image": { - "description": "Generate-PPTX context with provided, placeholder, or formula images and no acquisition role.", + "description": "Generate-PPTX context with provided or placeholder images and no acquisition role.", "scope": "cumulative", "include": [ "route.generate.planning-image", @@ -991,7 +1022,7 @@ "max_tokens": 1000 }, "stage.generate.strategist.image": { - "description": "Conditional Strategist image/formula layout math and compact composition vocabulary; the rendering decision core is already in base planning.", + "description": "Conditional Strategist image layout math and compact composition vocabulary; the rendering decision core is already in base planning.", "scope": "incremental", "files": [ "skills/ppt-master/references/image-layout-spec.md", @@ -1015,6 +1046,22 @@ ], "max_tokens": 6750 }, + "stage.generate.executor.formula": { + "description": "Conditional direct authoring contract for PowerPoint-native inline and block formulas.", + "scope": "incremental", + "files": [ + "skills/ppt-master/references/native-formula.md" + ], + "max_tokens": 1250 + }, + "stage.generate.executor.hyperlink": { + "description": "Conditional direct authoring contract for PowerPoint-native external and same-deck click hyperlinks.", + "scope": "incremental", + "files": [ + "skills/ppt-master/references/native-hyperlinks.md" + ], + "max_tokens": 1250 + }, "stage.generate.executor.image": { "description": "Conditional image embedding and execution rules with the always-read layout math and compact composition vocabulary.", "scope": "incremental", @@ -2021,6 +2068,10 @@ "glob": "skills/ppt-master/scripts/docs/prompt_audit.md", "reason": "Maintainer-only documentation for this audit tool." }, + { + "glob": "skills/ppt-master/scripts/docs/image.md", + "reason": "Standalone image-tool command reference; generation roles load the owning image contracts directly, and its formula renderer section is legacy-only." + }, { "glob": "skills/ppt-master/scripts/docs/mask-gradient-smoke.md", "reason": "Maintainer-only executable smoke; never loaded by generation roles." diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py index 9e2a8d85..3a6d0c7e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/source_to_md/ppt_to_md.py @@ -217,8 +217,11 @@ def _encode_md_url(url: str) -> str: return quote(url, safe="/:?=&%#@!$'*+,;") -def _resolve_internal_jump(run: object, shape: object) -> str | None: - """Return ``#slide-N`` for a run carrying a slide-internal jump, else None. +def _resolve_internal_jump( + run: object, + shape: object, +) -> tuple[bool, str | None]: + """Return whether a run is an internal jump and its resolved target. Reads ``run._r`` (private python-pptx API) because the public ``run.hyperlink.address`` cannot tell an internal jump apart from an @@ -228,26 +231,26 @@ def _resolve_internal_jump(run: object, shape: object) -> str | None: try: rpr = run._r.find(qn("a:rPr")) if rpr is None: - return None + return False, None hlink = rpr.find(qn("a:hlinkClick")) if hlink is None or "hlinksldjump" not in (hlink.get("action", "") or ""): - return None + return False, None r_id = hlink.get(qn("r:id"), "") if not r_id: - return None + return True, None target_slide = shape.part.related_part(r_id).slide prs = shape.part.slide.part.package.presentation_part.presentation - return f"#slide-{list(prs.slides).index(target_slide) + 1}" + return True, f"#slide-{list(prs.slides).index(target_slide) + 1}" except (KeyError, ValueError, AttributeError): print(f"[WARN] ppt_to_md: could not resolve slide jump rId={r_id}", file=sys.stderr) - return None + return True, None def _run_url(run: object, shape: object) -> str | None: """Resolve a run's hyperlink target to a markdown-ready URL, or None.""" if shape is not None: - internal = _resolve_internal_jump(run, shape) - if internal: + is_internal, internal = _resolve_internal_jump(run, shape) + if is_internal: return internal try: addr = run.hyperlink.address @@ -258,7 +261,12 @@ def _run_url(run: object, shape: object) -> str | None: return None -def _paragraph_to_markdown(paragraph: object, shape: object) -> str: +def _paragraph_to_markdown( + paragraph: object, + shape: object, + *, + use_shape_click_action: bool = True, +) -> str: """Render one paragraph, merging consecutive runs that share a URL. Run text is concatenated verbatim — including the spaces between runs — and @@ -298,28 +306,40 @@ def _paragraph_to_markdown(paragraph: object, shape: object) -> str: text = normalize_text("".join(parts)) # Shape-level click_action only matters when no run carried its own link. - if not has_run_hyperlink and shape is not None: + if ( + use_shape_click_action + and not has_run_hyperlink + and shape is not None + ): text = _apply_shape_click_action(text, shape) return text def _apply_shape_click_action(text: str, shape: object) -> str: """Wrap paragraph text in a link from the shape's click_action, if any.""" + target = _shape_click_target(shape) + if target is None: + return text + return f"[{_escape_md_link_text(text)}]({target})" + + +def _shape_click_target(shape: object) -> str | None: + """Return one Markdown-ready whole-shape click target, if supported.""" try: action = shape.click_action if action.action == PP_ACTION.HYPERLINK: url = action.hyperlink.address or "" if _is_supported_url(url): - return f"[{_escape_md_link_text(text)}]({_encode_md_url(url)})" + return _encode_md_url(url) elif action.action == PP_ACTION.NAMED_SLIDE: target = action.target_slide if target is not None: prs = shape.part.slide.part.package.presentation_part.presentation idx = list(prs.slides).index(target) + 1 - return f"[{_escape_md_link_text(text)}](#slide-{idx})" + return f"#slide-{idx}" except (AttributeError, ValueError): print("[WARN] ppt_to_md: could not process shape click_action", file=sys.stderr) - return text + return None def _paragraph_has_hyperlink(paragraph: object) -> bool: @@ -339,7 +359,12 @@ def _paragraph_has_hyperlink(paragraph: object) -> bool: return False -def text_frame_to_markdown(text_frame: object, shape: object = None) -> str: +def text_frame_to_markdown( + text_frame: object, + shape: object = None, + *, + use_shape_click_action: bool = True, +) -> str: """Convert a PowerPoint text frame into Markdown, preserving hyperlinks. Run-level external URLs and slide-internal jumps are emitted as @@ -362,7 +387,11 @@ def text_frame_to_markdown(text_frame: object, shape: object = None) -> str: paragraphs = [] for paragraph in visible_paragraphs: text = _escape_readback_control_lines( - _paragraph_to_markdown(paragraph, shape) + _paragraph_to_markdown( + paragraph, + shape, + use_shape_click_action=use_shape_click_action, + ) ) if not text: continue @@ -377,11 +406,20 @@ def text_frame_to_markdown(text_frame: object, shape: object = None) -> str: return "\n\n".join(paragraphs) -def table_to_markdown(table: object) -> str: +def table_to_markdown(table: object, shape: object = None) -> str: """Convert a PowerPoint table to a Markdown table.""" rows = [] for row in table.rows: - cells = [escape_table_cell(cell.text) for cell in row.cells] + cells = [ + escape_table_cell( + text_frame_to_markdown( + cell.text_frame, + shape, + use_shape_click_action=False, + ) + ) + for cell in row.cells + ] rows.append(cells) if not rows: @@ -1098,7 +1136,7 @@ def convert_presentation_to_markdown( shape = item.shape if getattr(shape, "has_table", False): - table_md = table_to_markdown(shape.table) + table_md = table_to_markdown(shape.table, shape) if table_md: blocks.append(table_md) continue @@ -1136,10 +1174,14 @@ def convert_presentation_to_markdown( image_count = next_image_index image_manifest.append(saved_picture.manifest_entry) asset_dir_used = True - blocks.append( + image_markdown = ( f"![Slide {slide_index} Image {image_ref_count}]" f"({asset_dir.name}/{saved_picture.filename})" ) + image_link = _shape_click_target(shape) + if image_link is not None: + image_markdown = f"[{image_markdown}]({image_link})" + blocks.append(image_markdown) if is_picture_shape: continue diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/flatten_tspan.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/flatten_tspan.py index b5e5d917..775e2af4 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/flatten_tspan.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/flatten_tspan.py @@ -189,6 +189,7 @@ PARAGRAPH_SOFT_BREAK_ATTR = "data-paragraph-soft-break" # Marks an authored visual line boundary that remains a hard DrawingML break # in the default single-frame preserve mode. PARAGRAPH_LINE_BREAK_ATTR = "data-paragraph-line-break" +INLINE_FORMULA_ATTR = "data-pptx-inline-formula" # Tolerance for detecting "base line-height" vs "paragraph gap": dy values # within ±DY_TOLERANCE_PX of each other are considered the same line-height. @@ -676,8 +677,42 @@ def flatten_text_with_tspans( def _has_tspan_children(elem: ET.Element) -> bool: - """Return True if elem contains any nested children (inline runs).""" - return any(c.tag == f"{{{SVG_NS}}}tspan" for c in list(elem)) + """Return True when one inline subtree has nested runs or hyperlinks.""" + return any( + c.tag in { + f"{{{SVG_NS}}}a", + f"{{{SVG_NS}}}tspan", + } + for c in list(elem) + ) + + +def _copy_inline_element(src: ET.Element, strip_line_attrs: bool) -> ET.Element: + """Deep-copy one supported inline ``tspan`` or hyperlink subtree.""" + local = src.tag.rsplit("}", 1)[-1] + if local not in {"a", "tspan"}: + raise ValueError(f"Unsupported inline text child <{local}>") + new = ET.Element(f"{{{SVG_NS}}}{local}") + consumed_dx = ( + local == "tspan" + and strip_line_attrs + and _positional_tspan_attribute(src) is not None + ) + for k, v in src.attrib.items(): + if strip_line_attrs and k in ("x", "y", "dy"): + continue + if k == "dx" and consumed_dx: + continue + new.set(k, v) + new.text = src.text + for child in list(src): + if child.tag in { + f"{{{SVG_NS}}}a", + f"{{{SVG_NS}}}tspan", + }: + new.append(_copy_inline_element(child, strip_line_attrs=False)) + new.tail = src.tail + return new def _copy_inline_tspan(src: ET.Element, strip_line_attrs: bool) -> ET.Element: @@ -689,23 +724,7 @@ def _copy_inline_tspan(src: ET.Element, strip_line_attrs: bool) -> ET.Element: dx on later inline runs. Nested tspans are copied recursively without stripping (they are already inline-only). """ - new = ET.Element(f"{{{SVG_NS}}}tspan") - consumed_dx = ( - strip_line_attrs - and _positional_tspan_attribute(src) is not None - ) - for k, v in src.attrib.items(): - if strip_line_attrs and k in ("x", "y", "dy"): - continue - if k == "dx" and consumed_dx: - continue - new.set(k, v) - new.text = src.text - for child in list(src): - if child.tag == f"{{{SVG_NS}}}tspan": - new.append(_copy_inline_tspan(child, strip_line_attrs=False)) - new.tail = src.tail - return new + return _copy_inline_element(src, strip_line_attrs) def _create_text_element_from_line( @@ -741,6 +760,7 @@ def _create_text_element_from_line( and len(tspans) == 1 and not _has_tspan_children(tspans[0]) and not tspans[0].tail + and tspans[0].get(INLINE_FORMULA_ATTR) is None ): tspan = tspans[0] content = collect_text_content(tspan) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/checker.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/checker.py index ae3d4b5c..30cd95fb 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/checker.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/checker.py @@ -119,6 +119,15 @@ except ImportError: _unsafe_exported_font_faces = None _validate_dml_shape_matrix = None +try: + from hyperlink_contract import ( + SHAPE_HYPERLINK_ATTR as _SHAPE_HYPERLINK_ATTR, + project_hyperlink_errors as _project_hyperlink_errors, + ) +except ImportError: + _SHAPE_HYPERLINK_ATTR = 'data-pptx-shape-hyperlink' + _project_hyperlink_errors = None + try: from svg_to_pptx.drawingml.converter import ( SvgNativeConversionError as _SvgNativeConversionError, @@ -209,13 +218,17 @@ except ImportError: try: from svg_to_pptx.native_objects import ( + INLINE_FORMULA_ATTR as _INLINE_FORMULA_ATTR, native_fallback_kind as _native_fallback_kind, + inline_formula_marker_errors as _inline_formula_marker_errors, native_marker_legacy_warnings as _native_marker_legacy_warnings, native_replacement_kind as _native_replacement_kind, native_replacement_status as _native_replacement_status, ) except ImportError: + _INLINE_FORMULA_ATTR = 'data-pptx-inline-formula' _native_fallback_kind = None + _inline_formula_marker_errors = None _native_marker_legacy_warnings = None _native_replacement_kind = None _native_replacement_status = None @@ -1054,6 +1067,7 @@ class SVGQualityChecker: self._communication_trace_issues: List[Tuple[str, str]] = [] self._pptx_structure_issues: List[Tuple[str, str]] = [] self._has_incomplete_page_roster = False + self._active_slide_count: int | None = None self._prototype_by_output: Dict[Path, Path] = {} self._active_prototype_path: Path | None = None self._active_template_reuse_scope: str | None = None @@ -1219,6 +1233,9 @@ class SVGQualityChecker: # 5. Check text wrapping methods self._check_text_elements(content, root, result) + # 5b. Validate native hyperlink targets and carrier structure. + self._check_hyperlinks(root, result) + # 6. Check image references (file existence and resolution) self._check_image_references(root, svg_path, result) @@ -1238,7 +1255,7 @@ class SVGQualityChecker: # 8b. Check elements declare a PPTX preset. self._check_pattern_fills(root, result) - # 8c. Check opt-in native table/chart markers before export. + # 8c. Check explicit native replacement markers before export. self._check_native_object_markers(root, result) # 8d. Validate explicit master/layout/placeholder metadata. @@ -1494,6 +1511,32 @@ class SVGQualityChecker: self._check_unmergeable_leading_text(root, result) self._check_nested_positional_tspans(root, result) + def _check_hyperlinks(self, root: ET.Element, result: Dict) -> None: + """Validate the standard SVG anchor surface shared with export.""" + anchors = [ + elem for elem in root.iter() + if _local_name(elem) == 'a' + ] + transports = [ + elem for elem in root.iter() + if elem.get(_SHAPE_HYPERLINK_ATTR) is not None + ] + if not anchors and not transports: + return + result['info']['hyperlinks'] = len(anchors) + len(transports) + if _project_hyperlink_errors is None: + result['errors'].append( + 'Unable to import hyperlink validator; cannot verify SVG links' + ) + return + result['errors'].extend( + f'Invalid SVG hyperlink: {error}' + for error in _project_hyperlink_errors( + root, + slide_count=self._active_slide_count, + ) + ) + def _check_nested_positional_tspans( self, root: ET.Element, @@ -3726,7 +3769,20 @@ class SVGQualityChecker: ) def _check_native_object_markers(self, root: ET.Element, result: Dict) -> None: - """Validate opt-in native table/chart markers before PPTX export.""" + """Validate explicit native replacement markers before PPTX export.""" + inline_formula_markers = [ + elem for elem in root.iter() + if elem.get(_INLINE_FORMULA_ATTR) is not None + ] + if inline_formula_markers and _inline_formula_marker_errors is None: + result['errors'].append( + "Unable to import inline-formula validator; cannot verify " + f"{_INLINE_FORMULA_ATTR} markers" + ) + elif _inline_formula_marker_errors is not None: + for error in _inline_formula_marker_errors(root): + result['errors'].append(f"Invalid inline formula marker: {error}") + invalid_status_elements: set[ET.Element] = set() for elem in root.iter(): marker_id = elem.get('id') or elem.get('data-name') or '' @@ -5029,6 +5085,8 @@ class SVGQualityChecker: self.issue_types['Input issues'] += 1 return [] + self._active_slide_count = len(svg_files) + self._configure_prototype_context(dir_path, svg_files) if not self.template_mode: self._prepare_undeclared_size_occurrences(svg_files) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/svg_contracts.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/svg_contracts.py index b135061a..fd8c0ad4 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/svg_contracts.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality/svg_contracts.py @@ -387,7 +387,7 @@ def check_reference_spelling(root: ET.Element, result: Dict) -> None: labels = [] xlink_href = f'{{{XLINK_NS}}}href' for elem in root.iter(): - if _local_name(elem).lower() not in {'image', 'use'}: + if _local_name(elem).lower() not in {'a', 'image', 'use'}: continue if elem.get(xlink_href) is not None: labels.append(_element_label(elem)) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py index 2b95c09e..bc0d8476 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py @@ -10,6 +10,8 @@ from pathlib import Path, PureWindowsPath from typing import Any from xml.etree import ElementTree as ET +from hyperlink_contract import SHAPE_HYPERLINK_ATTR + from pptx_animations import ( ANIMATIONS, ANIMATION_AFTER_EFFECTS, @@ -77,6 +79,7 @@ class GroupTarget: order: int chrome: bool = False structurally_static: bool = False + has_hyperlink: bool = False @dataclass(frozen=True) @@ -156,6 +159,11 @@ def scan_svg_targets(svg_path: Path) -> tuple[list[GroupTarget], list[str]]: order=visual_index, chrome=chrome, structurally_static=structurally_static, + has_hyperlink=any( + _tag_name(descendant) == 'a' + or descendant.get(SHAPE_HYPERLINK_ATTR) is not None + for descendant in child.iter() + ), ) ) @@ -1492,6 +1500,12 @@ def validate_animation_config( f'references non-triggerable structural group ' f'{trigger_shape!r}' ) + elif trigger_target.has_hyperlink: + warnings.append( + f'animations.json {effect_path}.trigger_shape ' + f'references hyperlink-bearing group {trigger_shape!r}; ' + 'use an ordinary animation or a separate trigger' + ) morph_pairs, morph_errors = _resolve_morph_pairs( list(targets_by_slide), diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py index 5181a1d7..62fc9b82 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py @@ -69,6 +69,8 @@ class ConvertContext: claimed_shape_ids: set[int] = field(default_factory=set) referenced_shape_ids: set[int] = field(default_factory=set) slide_num: int = 1 + # Public presentation roster size, used to fail closed on #slide-N links. + slide_count: int | None = None translate_x: float = 0.0 translate_y: float = 0.0 scale_x: float = 1.0 @@ -245,6 +247,7 @@ class ConvertContext: claimed_shape_ids=self.claimed_shape_ids, referenced_shape_ids=self.referenced_shape_ids, slide_num=self.slide_num, + slide_count=self.slide_count, translate_x=self.translate_x + dx, translate_y=self.translate_y + dy, scale_x=self.scale_x * sx, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py index d5342139..039e5a1e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py @@ -12,6 +12,11 @@ from typing import Any from xml.etree import ElementTree as ET from native_payloads import NativePayloadError, hydrate_native_payload_refs +from hyperlink_contract import ( + SHAPE_HYPERLINK_ATTR, + project_hyperlink_errors, + svg_hyperlink_href, +) from pptx_shapes import ( has_relationship_attributes, resolve_preset_preview_hash, @@ -33,6 +38,7 @@ from .context import ( ShapeResult, resolve_text_flow, ) +from .hyperlinks import apply_shape_hyperlink from .paths import ( project_freeform_geometry_errors, project_gradient_geometry_errors, @@ -92,8 +98,10 @@ from ..canvas_contract import ( parse_project_viewbox, ) from ..native_objects import ( + INLINE_FORMULA_ATTR, NativeMarkerAttributeError, convert_native_object, + inline_formula_marker_errors, native_metadata_payload_matches, native_replacement_kind, native_marker_transform, @@ -117,11 +125,11 @@ def _hydrate_native_payloads(root: ET.Element, svg_path: Path) -> int: ) from exc -def _require_chart_table_marker_attributes( +def _require_native_marker_attributes( root: ET.Element, svg_path: Path | str, ) -> None: - """Reject contradictory chart/table marker aliases before either route.""" + """Reject contradictory native marker aliases before conversion.""" errors: list[str] = [] for elem in root.iter(): if elem.tag.rsplit('}', 1)[-1] == 'metadata': @@ -147,11 +155,64 @@ def _require_chart_table_marker_attributes( preview = '; '.join(errors[:8]) suffix = '' if len(errors) <= 8 else f'; +{len(errors) - 8} more' raise SvgNativeConversionError( - f'{Path(svg_path).name}: invalid chart/table replacement metadata: ' + f'{Path(svg_path).name}: invalid native replacement metadata: ' f'{preview}{suffix}' ) +def _require_inline_formula_markers( + root: ET.Element, + svg_path: Path | str, +) -> None: + """Reject malformed inline formula runs before text lowering.""" + errors = inline_formula_marker_errors(root) + if not errors: + return + preview = '; '.join(errors[:8]) + suffix = '' if len(errors) <= 8 else f'; +{len(errors) - 8} more' + raise SvgNativeConversionError( + f'{Path(svg_path).name}: invalid {INLINE_FORMULA_ATTR} marker(s): ' + f'{preview}{suffix}' + ) + + +def _require_project_hyperlinks( + root: ET.Element, + svg_path: Path | str, + *, + slide_count: int | None, +) -> None: + """Reject hyperlinks that cannot be represented faithfully in PPTX.""" + errors = project_hyperlink_errors(root, slide_count=slide_count) + if not errors: + return + preview = '; '.join(errors[:8]) + suffix = '' if len(errors) <= 8 else f'; +{len(errors) - 8} more' + raise SvgNativeConversionError( + f'{Path(svg_path).name}: invalid SVG hyperlink(s): {preview}{suffix}' + ) + + +def _native_replacement_enabled(elem: ET.Element, ctx: ConvertContext) -> bool: + """Return whether this marker is active under the current export policy.""" + kind = native_replacement_kind(elem) + if kind == 'formula': + return True + return ctx.native_objects_enabled and kind in {'chart', 'table'} + + +def _contains_enabled_native_replacement( + elem: ET.Element, + ctx: ConvertContext, +) -> bool: + """Return whether one subtree contains an active native replacement.""" + return any( + descendant.tag.replace(f'{{{SVG_NS}}}', '') != 'metadata' + and _native_replacement_enabled(descendant, ctx) + for descendant in elem.iter() + ) + + def _require_project_freeform_geometry( root: ET.Element, svg_path: Path | str, @@ -693,11 +754,7 @@ def convert_g(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: keep their absolute slide coordinates unchanged. """ transform = elem.get('transform', '') - native_subtree_active = ctx.native_objects_enabled and any( - native_replacement_kind(descendant) - and descendant.tag.replace(f'{{{SVG_NS}}}', '') != 'metadata' - for descendant in elem.iter() - ) + native_subtree_active = _contains_enabled_native_replacement(elem, ctx) if native_subtree_active: dx, dy, sx, sy = native_marker_transform(transform) angle_deg = 0.0 @@ -789,12 +846,12 @@ def convert_g(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: if native_subtree_active and child_ctx.opacity_multiplier < 1.0: raise SvgNativeConversionError( - "Group opacity cannot be applied to data-pptx-replace-with chart/table " - "objects; export without --native-charts-and-tables to use the " - "shape-based SVG fallback" + "Group opacity cannot be applied to an active native replacement; " + "remove the group opacity or remove data-pptx-replace-with to keep " + "the subtree as ordinary SVG" ) - if child_ctx.native_objects_enabled: + if _native_replacement_enabled(elem, child_ctx): native_result = convert_native_object(elem, child_ctx) if native_result: ctx.sync_from_child(child_ctx) @@ -977,6 +1034,14 @@ def convert_g(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: ''', bounds_emu=(group_x, group_y, group_x + group_w, group_y + group_h)) +def convert_a(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: + """Convert one standard SVG anchor into a clickable DrawingML object.""" + result = convert_g(elem, ctx) + if result is None: + return None + return apply_shape_hyperlink(result, ctx, svg_hyperlink_href(elem)) + + # --------------------------------------------------------------------------- # Defs collection & element dispatch # --------------------------------------------------------------------------- @@ -994,6 +1059,7 @@ _CONVERTERS = { 'text': convert_text, 'image': convert_image, 'g': convert_g, + 'a': convert_a, 'svg': convert_nested_svg, } @@ -1188,6 +1254,18 @@ def _geometry_trace_metadata(elem: ET.Element, result: ShapeResult) -> dict[str, return {'output_geometry': 'picture', 'fidelity': 'native-normalized'} if xml.startswith(''): return {'output_geometry': 'native-object', 'fidelity': 'native-normalized'} + if xml.startswith(' ShapeResult | None if converter: try: result = converter(elem, ctx) + shape_hyperlink = elem.get(SHAPE_HYPERLINK_ATTR) + if result is not None and shape_hyperlink is not None: + result = apply_shape_hyperlink(result, ctx, shape_hyperlink) except Exception as e: trace('error', error=str(e)) raise SvgNativeConversionError(f'Failed to convert <{tag}>: {e}') from e @@ -1344,7 +1425,7 @@ def collect_unsupported_visuals( return is_supported_visual_child = ( tag in _SUPPORTED_VISUAL_CHILD_TAGS - and parent_tag in {'text', 'tspan'} + and parent_tag in {'text', 'tspan', 'a'} ) is_data_icon_placeholder = ( allow_data_icon_use @@ -1372,6 +1453,7 @@ def collect_unsupported_visuals( def convert_svg_to_slide_shapes( svg_path: str | Path, slide_num: int = 1, + slide_count: int | None = None, verbose: bool = False, merge_paragraphs: bool | None = None, image_optimize: bool = True, @@ -1400,6 +1482,7 @@ def convert_svg_to_slide_shapes( Args: svg_path: Path to the SVG file. slide_num: Slide number (for naming). + slide_count: Public deck size used to validate ``#slide-N`` targets. verbose: Print progress info. merge_paragraphs: Legacy compatibility option. True selects reflow; False selects split. Do not combine with ``text_flow``. @@ -1413,7 +1496,8 @@ def convert_svg_to_slide_shapes( image_scale: Target image pixels per SVG display pixel. image_quality: JPEG quality used for opaque optimized rasters. native_objects: Convert explicit ``data-pptx-replace-with`` chart/table - markers to native PowerPoint Chart/Table objects. Default off. + markers to native PowerPoint Chart/Table objects. Formula markers + are intrinsically native and do not use this opt-in. Default off. animation_group_overrides: Explicit top-level SVG group ids from ``animations.json`` that override the legacy chrome-name fallback. Explicit structural layer/role/placeholder markers remain excluded. @@ -1455,7 +1539,13 @@ def convert_svg_to_slide_shapes( ) except CanvasContractError as exc: raise SvgNativeConversionError(str(exc)) from exc - _require_chart_table_marker_attributes(root, svg_path) + _require_native_marker_attributes(root, svg_path) + _require_inline_formula_markers(root, svg_path) + _require_project_hyperlinks( + root, + svg_path, + slide_count=slide_count, + ) _require_project_nested_svg_crops(root, svg_path) _require_project_clip_paths(root, svg_path) authored_errors = validate_authored_preset_tree(root) @@ -1597,6 +1687,13 @@ def convert_svg_to_slide_shapes( if verbose: print(f' Expanded {expanded_local} local instance(s)') + _require_inline_formula_markers(root, svg_path) + _require_project_hyperlinks( + root, + svg_path, + slide_count=slide_count, + ) + # Recheck compiler-injected icon/use wrappers and cloned definition trees. _require_project_nested_svg_crops(root, svg_path) _require_project_images(root, svg_path) @@ -1621,6 +1718,18 @@ def convert_svg_to_slide_shapes( f'{svg_path.name}: text-metric materialization failed: {exc}' ) from exc + inline_formula_count_before_text_lowering = sum( + 1 for elem in root.iter() + if elem.get(INLINE_FORMULA_ATTR) is not None + ) + hyperlink_count_before_text_lowering = sum( + 1 for elem in root.iter() + if ( + elem.tag == f'{{{SVG_NS}}}a' + or elem.get(SHAPE_HYPERLINK_ATTR) is not None + ) + ) + # Flatten positional (those with x/y/non-zero dy) into independent # elements. DrawingML runs cannot reposition mid-paragraph, so a # dy-stacked block of tspans would otherwise collapse onto one baseline, @@ -1646,6 +1755,39 @@ def convert_svg_to_slide_shapes( if verbose: print(f' Lowered positional using {text_flow} text flow') + inline_formula_count_after_text_lowering = sum( + 1 for elem in root.iter() + if elem.get(INLINE_FORMULA_ATTR) is not None + ) + hyperlink_count_after_text_lowering = sum( + 1 for elem in root.iter() + if ( + elem.tag == f'{{{SVG_NS}}}a' + or elem.get(SHAPE_HYPERLINK_ATTR) is not None + ) + ) + if ( + inline_formula_count_after_text_lowering + != inline_formula_count_before_text_lowering + ): + raise SvgNativeConversionError( + f'{svg_path.name}: positional text lowering changed inline formula ' + f'marker count from {inline_formula_count_before_text_lowering} ' + f'to {inline_formula_count_after_text_lowering}' + ) + if hyperlink_count_after_text_lowering != hyperlink_count_before_text_lowering: + raise SvgNativeConversionError( + f'{svg_path.name}: positional text lowering changed hyperlink ' + f'count from {hyperlink_count_before_text_lowering} ' + f'to {hyperlink_count_after_text_lowering}' + ) + _require_inline_formula_markers(root, svg_path) + _require_project_hyperlinks( + root, + svg_path, + slide_count=slide_count, + ) + _require_project_text_properties(root, svg_path) try: text_font_sizes = resolve_project_font_sizes(root) @@ -1673,6 +1815,7 @@ def convert_svg_to_slide_shapes( reserved_shape_ids=frozenset(source_shape_id_map.values()), source_shape_id_map=source_shape_id_map, slide_num=slide_num, + slide_count=slide_count, viewport_width=viewport_width, viewport_height=viewport_height, svg_dir=Path(svg_path).parent, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/elements.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/elements.py index 7f540b35..7266c07f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/elements.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/elements.py @@ -24,6 +24,7 @@ from pptx_shapes import ( validate_ooxml_xfrm, ) from pptx_effects import EFFECT_REASON_ATTR, EFFECT_STATUS_ATTR +from hyperlink_contract import svg_hyperlink_href from pptx_to_svg.preset_authoring import AUTHORING_ATTR, AUTHORING_VALUE from resource_paths import ( resolve_external_image_reference, @@ -36,6 +37,12 @@ from .context import ( ConvertContext, ShapeResult, ) +from .hyperlinks import ( + HYPERLINK_ACTION_KEY, + HYPERLINK_RID_KEY, + hyperlink_click_xml, + hyperlink_run_metadata, +) from .theme_colors import color_node_xml from .theme_fonts import theme_font_tokens from .text_properties import ( @@ -1957,6 +1964,8 @@ _TEXT_BULLET_MARKERS = { _TEXT_BULLET_RE = re.compile( r'^(?P\s*)(?P[·•●▪■◆◇◦‣])(?P\s*)' ) +_INLINE_FORMULA_ATTR = 'data-pptx-inline-formula' +_INLINE_FORMULA_KEY = '_inline_formula_latex' def _normalize_text_run_whitespace( @@ -2172,6 +2181,12 @@ def _extract_text_bullet( runs: list[dict[str, Any]], ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: """Convert a leading text bullet marker into paragraph metadata.""" + first_nonspace = _first_nonspace_run(runs) + if first_nonspace and ( + first_nonspace.get(_INLINE_FORMULA_KEY) is not None + or first_nonspace.get(HYPERLINK_RID_KEY) is not None + ): + return runs, None full_text = ''.join(str(run.get('text', '')) for run in runs) match = _TEXT_BULLET_RE.match(full_text) if not match: @@ -2385,33 +2400,68 @@ def _collect_tspan_runs( ctx: ConvertContext, inherited_xml_space: str = 'default', ) -> list[dict[str, Any]]: - """Recursively turn a tspan subtree into runs, propagating styling through nested tspans. + """Recursively turn one inline SVG subtree into DrawingML text runs.""" + return _collect_inline_runs( + tspan, + inherited_attrs, + ctx, + inherited_xml_space, + ) - Order: tspan.text → (each nested child tspan's runs → that child's tail under THIS tspan's attrs). - """ + +def _collect_inline_runs( + container: ET.Element, + inherited_attrs: dict[str, Any], + ctx: ConvertContext, + inherited_xml_space: str = 'default', + inherited_hyperlink: dict[str, str] | None = None, +) -> list[dict[str, Any]]: + """Collect nested ``tspan``/``a`` content with style and link inheritance.""" runs: list[dict[str, Any]] = [] - own_attrs = _override_run_attrs(inherited_attrs, tspan, ctx) - own_xml_space = resolve_project_xml_space(tspan, inherited_xml_space) + own_attrs = _override_run_attrs(inherited_attrs, container, ctx) + own_xml_space = resolve_project_xml_space(container, inherited_xml_space) + container_tag = container.tag.replace(f'{{{SVG_NS}}}', '') + own_hyperlink = inherited_hyperlink + if container_tag == 'a': + own_hyperlink = hyperlink_run_metadata( + ctx, + svg_hyperlink_href(container), + ) - if tspan.text: - runs.append({ + if container.text: + run = { **own_attrs, - 'text': tspan.text, + 'text': container.text, '_xml_space': own_xml_space, - }) + } + if own_hyperlink is not None: + run.update(own_hyperlink) + inline_formula = container.get(_INLINE_FORMULA_ATTR) + if inline_formula is not None: + run[_INLINE_FORMULA_KEY] = inline_formula + runs.append(run) - for child in tspan: + for child in container: child_tag = child.tag.replace(f'{{{SVG_NS}}}', '') - if child_tag == 'tspan': + if child_tag in {'tspan', 'a'}: runs.extend( - _collect_tspan_runs(child, own_attrs, ctx, own_xml_space) + _collect_inline_runs( + child, + own_attrs, + ctx, + own_xml_space, + own_hyperlink, + ) ) if child.tail: - runs.append({ + tail_run = { **own_attrs, 'text': child.tail, '_xml_space': own_xml_space, - }) + } + if own_hyperlink is not None: + tail_run.update(own_hyperlink) + runs.append(tail_run) return runs @@ -2439,10 +2489,13 @@ def _build_text_runs( for child in elem: child_tag = child.tag.replace(f'{{{SVG_NS}}}', '') - if child_tag == 'tspan': - runs.extend( - _collect_tspan_runs(child, parent_attrs, ctx, xml_space) - ) + if child_tag in {'tspan', 'a'}: + runs.extend(_collect_inline_runs( + child, + parent_attrs, + ctx, + xml_space, + )) if child.tail: runs.append({ **parent_attrs, @@ -2540,6 +2593,7 @@ def _build_run_properties_xml( default_fonts: dict[str, str], ctx: ConvertContext | None = None, effect_xml: str = '', + fixed_font_family: str | None = None, ) -> str: """Build the final ``a:rPr`` used to compare and emit one text run.""" text = str(run['text']) @@ -2569,14 +2623,22 @@ def _build_run_properties_xml( spc_attr = _letter_spacing_to_drawingml_spc(letter_spacing_px) fonts = parse_font_family(ff) if ff else default_fonts - run_fonts = theme_font_tokens( - fonts, - ctx.theme_font_spec if ctx is not None else None, - ) or resolve_text_run_fonts(text, fonts) - lang = detect_text_lang( + run_fonts = ( + { + 'latin': fixed_font_family, + 'ea': fixed_font_family, + 'cs': fixed_font_family, + } + if fixed_font_family is not None + else theme_font_tokens( + fonts, + ctx.theme_font_spec if ctx is not None else None, + ) or resolve_text_run_fonts(text, fonts) + ) + lang = str(run.get('_language_override') or detect_text_lang( text, ctx.primary_language if ctx is not None else None, - ) + )) rtl_xml = ( '\n' if text_has_rtl_characters(text) @@ -2585,6 +2647,17 @@ def _build_run_properties_xml( fill_xml = _build_text_fill_xml(fill, fill_raw, opacity, ctx) outline_xml = _build_text_outline_xml(run, ctx) + relationship_id = run.get(HYPERLINK_RID_KEY) + hyperlink_xml = ( + hyperlink_click_xml( + str(relationship_id), + str(run.get(HYPERLINK_ACTION_KEY)) + if run.get(HYPERLINK_ACTION_KEY) is not None + else None, + ) + if relationship_id is not None + else '' + ) return f''' {outline_xml} @@ -2592,7 +2665,8 @@ def _build_run_properties_xml( {effect_xml} -{rtl_xml} + +{hyperlink_xml}{rtl_xml} ''' @@ -2612,8 +2686,18 @@ def _coalesce_text_runs( text = str(run.get('text', '')) if not text: continue + if run.get(_INLINE_FORMULA_KEY) is not None: + merged.append({**run, 'text': text}) + previous_properties = None + continue properties = _build_run_properties_xml(run, default_fonts, ctx) - if merged and properties == previous_properties: + if ( + merged + and merged[-1].get(_INLINE_FORMULA_KEY) is None + and merged[-1].get(HYPERLINK_RID_KEY) == run.get(HYPERLINK_RID_KEY) + and merged[-1].get(HYPERLINK_ACTION_KEY) == run.get(HYPERLINK_ACTION_KEY) + and properties == previous_properties + ): candidate = { **merged[-1], 'text': str(merged[-1].get('text', '')) + text, @@ -2652,6 +2736,37 @@ def _build_run_xml( if run.get('_line_break'): return '' text = str(run['text']) + inline_formula = run.get(_INLINE_FORMULA_KEY) + if inline_formula is not None: + fill_raw = str(run.get('fill_raw') or f"#{run.get('fill', '000000')}") + fill_color, fill_alpha = parse_svg_color(fill_raw) + if fill_color is None or fill_alpha <= 0: + raise ValueError( + 'inline formula text requires one visible solid fill color' + ) + math_run = { + **run, + 'font_family': 'Cambria Math', + 'font_weight': '400', + 'font_style': 'normal', + 'text_decoration': 'none', + 'letter_spacing': 0.0, + 'stroke_raw': '', + 'stroke_opacity': None, + '_language_override': ( + ctx.primary_language + if ctx is not None and ctx.primary_language is not None + else 'en-US' + ), + } + properties_xml = _build_run_properties_xml( + math_run, + default_fonts, + ctx, + fixed_font_family='Cambria Math', + ) + from ..native_objects.inline_formula import build_inline_formula_xml + return build_inline_formula_xml(str(inline_formula), properties_xml) properties_xml = _build_run_properties_xml( run, default_fonts, @@ -2802,11 +2917,15 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: if prev and not prev_text.endswith(' ') \ and not next_text.startswith(' ') \ and not boundary_is_cjk: - prev.append({ + joining_space = { **prev[-1], 'text': ' ', 'letter_spacing': 0.0, - }) + } + joining_space.pop(_INLINE_FORMULA_KEY, None) + joining_space.pop(HYPERLINK_RID_KEY, None) + joining_space.pop(HYPERLINK_ACTION_KEY, None) + prev.append(joining_space) prev.extend(line_runs) else: paragraph_runs.append(line_runs) @@ -3036,6 +3155,11 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: (run for run in line if not run.get('_line_break')), None, ) + effective_line_spacing = ( + '' + if any(run.get(_INLINE_FORMULA_KEY) is not None for run in line) + else ln_spc_xml + ) p_pr_xml = _paragraph_pr_xml( algn=algn, font_size=( @@ -3043,7 +3167,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: if first_text_run is not None else font_size ), - body_xml=f'{ln_spc_xml}{spc_bef_xml}', + body_xml=f'{effective_line_spacing}{spc_bef_xml}', bullet=bullet, ctx=ctx, rtl=text_uses_rtl( @@ -3128,7 +3252,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: 'anchor="t" anchorCtr="0">\n\n' ) - return ShapeResult(xml=f''' + shape_xml = f''' @@ -3146,7 +3270,14 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: {paragraphs_xml} -''', bounds_emu=(off_x, off_y, off_x + ext_cx, off_y + ext_cy)) +''' + if any(run.get(_INLINE_FORMULA_KEY) is not None for run in runs): + from ..native_objects.inline_formula import wrap_inline_formula_shape + shape_xml = wrap_inline_formula_shape(shape_xml) + return ShapeResult( + xml=shape_xml, + bounds_emu=(off_x, off_y, off_x + ext_cx, off_y + ext_cy), + ) # --------------------------------------------------------------------------- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/hyperlinks.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/hyperlinks.py new file mode 100644 index 00000000..a1123973 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/hyperlinks.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +PPT Master - DrawingML Hyperlink Lowering + +Register native PowerPoint hyperlink relationships and attach click actions to +text runs or clickable leaf shapes produced by the SVG converter. + +Usage: + from .hyperlinks import apply_shape_hyperlink, hyperlink_run_metadata + +Examples: + metadata = hyperlink_run_metadata(ctx, "https://example.com") + linked = apply_shape_hyperlink(result, ctx, "#slide-2") + +Dependencies: + PPT Master hyperlink contract and DrawingML conversion context. +""" + +from __future__ import annotations + +import re + +from hyperlink_contract import ( + HYPERLINK_REL_TYPE, + SLIDE_JUMP_ACTION, + SLIDE_REL_TYPE, + parse_hyperlink_target, +) + +from .context import ConvertContext, ShapeResult + + +HYPERLINK_RID_KEY = "_hyperlink_rid" +HYPERLINK_ACTION_KEY = "_hyperlink_action" + +_CNVPR_RE = re.compile( + r"]*(?:/>|>.*?)", + re.DOTALL, +) +_GROUP_NONVISUAL_RE = re.compile( + r".*?", + re.DOTALL, +) + + +def register_hyperlink( + ctx: ConvertContext, + raw_target: str, +) -> tuple[str, str | None]: + """Register one slide relationship and return ``(rId, action)``.""" + target = parse_hyperlink_target( + raw_target, + slide_count=ctx.slide_count, + ) + if target.kind == "slide": + relationship_type = SLIDE_REL_TYPE + relationship_target = f"slide{target.slide_number}.xml" + target_mode = None + action = SLIDE_JUMP_ACTION + else: + relationship_type = HYPERLINK_REL_TYPE + relationship_target = target.raw + target_mode = "External" + action = None + + for relationship in ctx.rel_entries: + if ( + relationship.get("type") == relationship_type + and relationship.get("target") == relationship_target + and relationship.get("target_mode") == target_mode + ): + return relationship["id"], action + + relationship_id = ctx.next_rel_id() + relationship = { + "id": relationship_id, + "type": relationship_type, + "target": relationship_target, + } + if target_mode is not None: + relationship["target_mode"] = target_mode + ctx.rel_entries.append(relationship) + return relationship_id, action + + +def hyperlink_click_xml( + relationship_id: str, + action: str | None = None, +) -> str: + """Build one DrawingML click hyperlink child.""" + action_attr = f' action="{action}"' if action else "" + return f'' + + +def hyperlink_run_metadata( + ctx: ConvertContext, + raw_target: str, +) -> dict[str, str]: + """Return the internal run fields for one SVG inline anchor.""" + relationship_id, action = register_hyperlink(ctx, raw_target) + metadata = {HYPERLINK_RID_KEY: relationship_id} + if action is not None: + metadata[HYPERLINK_ACTION_KEY] = action + return metadata + + +def apply_shape_hyperlink( + result: ShapeResult, + ctx: ConvertContext, + raw_target: str, +) -> ShapeResult: + """Attach one click hyperlink to every clickable leaf object. + + DrawingML group containers are selection/animation structures, not a + reliable click-action carrier across PowerPoint APIs. A multi-object SVG + anchor therefore shares one relationship across each leaf ``p:cNvPr``. + Authors use an explicit background shape when the whole rectangular button + area, including gaps between descendants, must be clickable. + """ + relationship_id, action = register_hyperlink(ctx, raw_target) + hlink_xml = hyperlink_click_xml(relationship_id, action) + group_ranges = [ + (match.start(), match.end()) + for match in _GROUP_NONVISUAL_RE.finditer(result.xml) + ] + replacements: list[tuple[int, int, str]] = [] + for match in _CNVPR_RE.finditer(result.xml): + if any(start <= match.start() < end for start, end in group_ranges): + continue + original = match.group(0) + if ""): + replacement = f"{original[:-2]}>{hlink_xml}" + else: + replacement = original.replace( + "", + f"{hlink_xml}", + 1, + ) + replacements.append((match.start(), match.end(), replacement)) + + if not replacements: + raise ValueError( + "hyperlink carrier did not produce a clickable DrawingML leaf" + ) + linked_xml = result.xml + for start, end, replacement in reversed(replacements): + linked_xml = linked_xml[:start] + replacement + linked_xml[end:] + return ShapeResult(xml=linked_xml, bounds_emu=result.bounds_emu) + + +__all__ = [ + "HYPERLINK_ACTION_KEY", + "HYPERLINK_RID_KEY", + "apply_shape_hyperlink", + "hyperlink_click_xml", + "hyperlink_run_metadata", + "register_hyperlink", +] diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/utils.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/utils.py index 8914eaf0..098c467d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/utils.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/utils.py @@ -1026,7 +1026,7 @@ def _contains_native_marker(elem: ET.Element) -> bool: from ..native_objects.marker_attributes import native_replacement_kind return any( - native_replacement_kind(descendant) in {'table', 'chart'} + native_replacement_kind(descendant) in {'table', 'chart', 'formula'} for descendant in _iter_visual_transform_tree(elem) ) @@ -1177,7 +1177,7 @@ def _transform_semantic_error( if all(name in {'translate', 'scale'} for name in names): return None return ( - f'{label} native table/chart marker transforms support only ' + f'{label} native replacement marker transforms support only ' 'translate and scale' ) if _contains_thick_circle(elem, thick_circle_ids): diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py index b5852381..bae05579 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py @@ -1,4 +1,4 @@ -"""Native PowerPoint table/chart converters for explicit SVG metadata markers.""" +"""Native PowerPoint object converters for explicit SVG metadata markers.""" from __future__ import annotations @@ -8,12 +8,13 @@ from xml.etree import ElementTree as ET from ..drawingml.context import ConvertContext, ShapeResult from ..drawingml.utils import _xml_escape -from .chart_data import _chart_data +from .chart_data import _chart_data, _chart_plot_area_layout from .chart_style import ( _axis_titles, _chart_companion_entries, _chart_companion_text_xml, _chart_text_sizes, + _chart_title_is_bounded, _classic_chart_style, _native_chart_chrome_errors, _native_chart_chrome_warnings, @@ -33,6 +34,11 @@ from .fallback_hash import ( snapshot_native_fallback_freshness, stamp_native_fallback_baseline, ) +from .formula import FormulaSpec, build_native_formula, validate_formula_payload +from .inline_formula import ( + INLINE_FORMULA_ATTR, + inline_formula_marker_errors, +) from .marker_common import ( CHART_CONTENT_TYPE, CHARTEX_CONTENT_TYPE, @@ -73,6 +79,7 @@ from .workbook import ( __all__ = [ "convert_native_object", + "INLINE_FORMULA_ATTR", "NativeMarkerAttributeError", "native_fallback_kind", "native_import_source", @@ -82,6 +89,7 @@ __all__ = [ "native_replacement_kind", "native_replacement_status", "native_marker_transform", + "inline_formula_marker_errors", "snapshot_native_fallback_freshness", "stamp_native_fallback_baseline", "validate_native_object_marker", @@ -152,6 +160,7 @@ def _build_native_chart(elem: ET.Element, ctx: ConvertContext, payload: dict[str chart_data=chart_data, inherited_styles=ctx.inherited_styles, primary_language=ctx.primary_language, + chart_bounds=(off_x, off_y, ext_cx, ext_cy), ) ctx.package_files[chart_rels_part] = _chart_rels_xml(f"../embeddings/{workbook_name}") if chart_data["kind"] == "xy": @@ -183,7 +192,10 @@ def _build_native_chart(elem: ET.Element, ctx: ConvertContext, payload: dict[str chart_style=chart_style, note_font_size=text_sizes["note"], title_font_size=text_sizes["title"], - include_title=chart_data["kind"] == "chartex", + include_title=( + chart_data["kind"] == "chartex" + or _chart_title_is_bounded(payload) + ), include_subtitle_as_caption=chart_data["kind"] == "chartex", ) xml = chart_frame_xml + companion_xml @@ -197,7 +209,7 @@ def _validate_native_object_marker_payload( ctx: ConvertContext | None = None, ancestors: tuple[ET.Element, ...] = (), require_fresh_fallback: bool = False, -) -> tuple[str, dict[str, Any], list[list[Any]] | None]: +) -> tuple[str, dict[str, Any], list[list[Any]] | FormulaSpec | None]: try: kind = native_replacement_kind(elem) except NativeMarkerAttributeError as exc: @@ -210,9 +222,9 @@ def _validate_native_object_marker_payload( if kind not in _NATIVE_KINDS: raise RuntimeError(f"Unsupported data-pptx-replace-with value: {kind}") if _local_tag(elem) != "g": - raise RuntimeError("Native PPTX table/chart markers must be elements") + raise RuntimeError("Native PPTX replacement markers must be elements") native_marker_transform(elem.get("transform")) - if require_fresh_fallback: + if require_fresh_fallback and kind in {"chart", "table"}: require_fresh_native_fallback(elem, use_runtime_snapshot=True) try: @@ -221,26 +233,36 @@ def _validate_native_object_marker_payload( raise RuntimeError(str(exc)) from exc bounds_ctx = ctx or _native_marker_validation_context(elem, ancestors) off_x, off_y, ext_cx, ext_cy, _ = _validate_bounds_inputs(elem, payload, bounds_ctx) - table_rows = None + validated_data: list[list[Any]] | FormulaSpec | None = None if kind == "table": table_rows, col_count, _merge_layout = _validate_table_payload(payload) + validated_data = table_rows if ext_cx < col_count or ext_cy < len(table_rows): raise RuntimeError( "Native PPTX table bounds must provide at least one EMU per row and column" ) - else: + elif kind == "chart": chart_data = _chart_data(payload) + _chart_plot_area_layout( + chart_data, + (off_x, off_y, ext_cx, ext_cy), + ) _validate_chart_companion_boxes( payload, chart_bounds=(off_x, off_y, ext_cx, ext_cy), - include_title=chart_data["kind"] == "chartex", + include_title=( + chart_data["kind"] == "chartex" + or _chart_title_is_bounded(payload) + ), include_subtitle_as_caption=chart_data["kind"] == "chartex", ) if validate_chrome and native_import_source(elem) != "pptx": chrome_errors = _native_chart_chrome_errors(elem, payload) if chrome_errors: raise RuntimeError("; ".join(chrome_errors)) - return kind, payload, table_rows + else: + validated_data = validate_formula_payload(payload, ctx=ctx) + return kind, payload, validated_data def validate_native_object_marker( @@ -248,7 +270,7 @@ def validate_native_object_marker( *, ancestors: tuple[ET.Element, ...] = (), ) -> None: - """Validate a chart/table replacement marker without mutating the package.""" + """Validate a native replacement marker without mutating the package.""" _validate_native_object_marker_payload(elem, ancestors=ancestors) @@ -258,8 +280,8 @@ def validate_native_object_marker_with_warnings( ancestors: tuple[ET.Element, ...] = (), document_root: ET.Element | None = None, ) -> list[str]: - """Validate a chart/table replacement marker and return non-fatal warnings.""" - kind, payload, table_rows = _validate_native_object_marker_payload( + """Validate a native replacement marker and return non-fatal warnings.""" + kind, payload, validated_data = _validate_native_object_marker_payload( elem, ancestors=ancestors, ) @@ -268,10 +290,10 @@ def validate_native_object_marker_with_warnings( elem, document_root=document_root, ) - if kind else [] + if kind in {"chart", "table"} else [] ) - if kind == "table" and table_rows is not None: - warnings.extend(_native_table_warnings(elem, table_rows)) + if kind == "table" and isinstance(validated_data, list): + warnings.extend(_native_table_warnings(elem, validated_data)) elif kind == "chart": warnings.extend(_native_chart_chrome_warnings(elem, payload)) return warnings @@ -283,7 +305,7 @@ def native_object_marker_warnings( ancestors: tuple[ET.Element, ...] = (), document_root: ET.Element | None = None, ) -> list[str]: - """Return non-fatal warnings for a chart/table replacement marker.""" + """Return non-fatal warnings for a native replacement marker.""" return validate_native_object_marker_with_warnings( elem, ancestors=ancestors, @@ -292,7 +314,7 @@ def native_object_marker_warnings( def convert_native_object(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: - """Convert a marked SVG group to a native PowerPoint table or chart.""" + """Convert a marked SVG group to its native PowerPoint object.""" try: kind = native_replacement_kind(elem) except NativeMarkerAttributeError as exc: @@ -300,12 +322,18 @@ def convert_native_object(elem: ET.Element, ctx: ConvertContext) -> ShapeResult if not kind: return None - kind, payload, _ = _validate_native_object_marker_payload( + kind, payload, validated_data = _validate_native_object_marker_payload( elem, validate_chrome=False, ctx=ctx, require_fresh_fallback=True, ) + if kind == "formula": + formula_spec = ( + validated_data if isinstance(validated_data, FormulaSpec) else None + ) + return build_native_formula(elem, ctx, payload, formula_spec) + marker_id = elem.get("id") or "" for warning in native_fallback_contract_warnings( elem, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py index 92c42992..605f40a5 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py @@ -12,6 +12,7 @@ from .marker_common import ( _first_present, _hex_or_none, _number, + _powerpoint_emu, _powerpoint_line_width_emu, ) @@ -235,6 +236,8 @@ _AXIS_ROLE_DEFAULTS = { def _chart_axes( payload: dict[str, Any], allowed_roles: set[str], + *, + bar_orientation: bool = False, ) -> dict[str, dict[str, Any]]: """Normalize the narrow classic-chart axis contract.""" raw_axes = payload.get("axes") @@ -264,6 +267,10 @@ def _chart_axes( f"Native PPTX chart axes.{role} contains unsupported field(s): {fields}" ) default_kind, default_position = _AXIS_ROLE_DEFAULTS[role] + if bar_orientation and role == "category": + default_position = "left" + elif bar_orientation and role == "value": + default_position = "bottom" kind = _compact_key(raw_config.get("kind") or default_kind) if kind not in {"date", "text", "value"}: raise RuntimeError( @@ -291,11 +298,16 @@ def _chart_axes( raise RuntimeError( f"Native PPTX chart axes.{role}.position must be bottom, left, right, or top" ) - allowed_positions = ( - {"bottom", "top"} - if role in {"category", "secondary_category", "x"} - else {"left", "right"} - ) + if bar_orientation and role == "category": + allowed_positions = {"left", "right"} + elif bar_orientation and role == "value": + allowed_positions = {"bottom", "top"} + else: + allowed_positions = ( + {"bottom", "top"} + if role in {"category", "secondary_category", "x"} + else {"left", "right"} + ) if position not in allowed_positions: choices = ", ".join(sorted(allowed_positions)) raise RuntimeError( @@ -372,6 +384,101 @@ def _category_axis_is_date(axes: dict[str, dict[str, Any]]) -> bool: return axes.get("category", {}).get("kind") == "date" +def _chart_plot_area(payload: dict[str, Any]) -> dict[str, float] | None: + """Normalize an optional absolute slide-local plot-area box.""" + raw = payload.get("plot_area") + if raw is None: + return None + if not isinstance(raw, dict): + raise RuntimeError("Native PPTX chart plot_area must be an object") + + box_keys = {"x", "y", "width", "height"} + unknown_keys = set(raw) - box_keys + if unknown_keys: + fields = ", ".join(sorted(unknown_keys)) + raise RuntimeError( + f"Native PPTX chart plot_area contains unsupported field(s): {fields}" + ) + missing_keys = box_keys - set(raw) + if missing_keys: + fields = ", ".join(sorted(missing_keys)) + raise RuntimeError( + "Native PPTX chart plot_area requires x/y/width/height together; " + f"missing: {fields}" + ) + + plot_area = { + key: _number(raw[key], f"chart plot_area.{key}") + for key in ("x", "y", "width", "height") + } + if plot_area["width"] <= 0 or plot_area["height"] <= 0: + raise RuntimeError("Native PPTX chart plot_area width/height must be positive") + return plot_area + + +def _chart_plot_area_layout( + chart_data: dict[str, Any], + chart_bounds: tuple[int, int, int, int], +) -> tuple[float, float, float, float] | None: + """Resolve an absolute plot-area box to chart-relative manual-layout factors.""" + plot_area = chart_data.get("plot_area") + if plot_area is None: + return None + + chart_x, chart_y, chart_width, chart_height = chart_bounds + plot_x = _powerpoint_emu(plot_area["x"], "chart plot_area.x") + plot_y = _powerpoint_emu(plot_area["y"], "chart plot_area.y") + plot_width = _powerpoint_emu( + plot_area["width"], + "chart plot_area.width", + positive=True, + ) + plot_height = _powerpoint_emu( + plot_area["height"], + "chart plot_area.height", + positive=True, + ) + if ( + plot_x < chart_x + or plot_y < chart_y + or plot_x + plot_width > chart_x + chart_width + or plot_y + plot_height > chart_y + chart_height + ): + raise RuntimeError( + "Native PPTX chart plot_area must be fully contained within the chart frame" + ) + return ( + (plot_x - chart_x) / chart_width, + (plot_y - chart_y) / chart_height, + plot_width / chart_width, + plot_height / chart_height, + ) + + +def _doughnut_hole_size(payload: dict[str, Any], chart_type: str) -> int | None: + """Normalize the closed doughnut-hole percentage contract.""" + raw = payload.get("hole_size") + if chart_type != "doughnut": + if raw is not None: + raise RuntimeError( + "Native PPTX chart hole_size is supported for doughnut charts only" + ) + return None + if raw is None: + return 75 + if not isinstance(raw, (int, float)) or isinstance(raw, bool): + raise RuntimeError("Native PPTX doughnut hole_size must be a numeric integer") + value = _chart_number(raw) + if not float(value).is_integer(): + raise RuntimeError("Native PPTX doughnut hole_size must be an integer") + hole_size = int(value) + if not 10 <= hole_size <= 90: + raise RuntimeError( + "Native PPTX doughnut hole_size must be between 10 and 90" + ) + return hole_size + + def _chart_kind(payload: dict[str, Any]) -> tuple[str, str | None, str | None]: raw_type = payload.get("type") or payload.get("chart_type") or "column" key = _compact_key(raw_type) @@ -655,8 +762,12 @@ def _category_chart_data( alias_grouping: str | None, alias_style: str | None, ) -> dict[str, Any]: - axes = _chart_axes(payload, {"category", "value"}) - if axes and chart_type in {"bar", "doughnut", "of_pie", "pie"}: + axes = _chart_axes( + payload, + {"category", "value"}, + bar_orientation=chart_type == "bar", + ) + if axes and chart_type in {"doughnut", "of_pie", "pie"}: raise RuntimeError( f"Native PPTX {chart_type} chart axes are outside current support" ) @@ -716,6 +827,7 @@ def _category_chart_data( "categories": categories, "grouping": grouping, "of_pie_type": of_pie_type, + "hole_size": _doughnut_hole_size(payload, chart_type), "line_style": line_style, "radar_marker_style": radar_marker_style, "radar_style": radar_style, @@ -1275,6 +1387,15 @@ def _xy_chart_data( def _chart_data(payload: dict[str, Any]) -> dict[str, Any]: chart_type, alias_grouping, alias_style = _chart_kind(payload) + if payload.get("hole_size") is not None and chart_type != "doughnut": + raise RuntimeError( + "Native PPTX chart hole_size is supported for doughnut charts only" + ) + plot_area = _chart_plot_area(payload) + if plot_area is not None and chart_type in _CHARTEX_CHART_TYPES: + raise RuntimeError( + "Native PPTX chart plot_area is supported for classic charts only" + ) if ( chart_type not in _CATEGORY_CHART_TYPES | {"combo", "stock"} | _XY_CHART_TYPES and _data_labels_config(payload) is not None @@ -1283,14 +1404,27 @@ def _chart_data(payload: dict[str, Any]) -> dict[str, Any]: f"Native PPTX {chart_type} chart data labels are outside current support" ) if chart_type == "combo": - return _combo_chart_data(payload) + chart_data = _combo_chart_data(payload) + chart_data["plot_area"] = plot_area + return chart_data if chart_type in _CHARTEX_CHART_TYPES: return _chartex_chart_data(payload, chart_type) if chart_type == "stock": - return _stock_chart_data(payload) + chart_data = _stock_chart_data(payload) + chart_data["plot_area"] = plot_area + return chart_data if chart_type in _XY_CHART_TYPES: - return _xy_chart_data(payload, chart_type, alias_style) - return _category_chart_data(payload, chart_type, alias_grouping, alias_style) + chart_data = _xy_chart_data(payload, chart_type, alias_style) + chart_data["plot_area"] = plot_area + return chart_data + chart_data = _category_chart_data( + payload, + chart_type, + alias_grouping, + alias_style, + ) + chart_data["plot_area"] = plot_area + return chart_data def validate_chart_payload(payload: dict[str, Any]) -> None: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py index 9b311b72..1bd685fe 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py @@ -340,6 +340,12 @@ def _chart_text_entry(value: Any) -> tuple[str, dict[str, Any]] | None: return str(value).strip(), {} +def _chart_title_is_bounded(payload: dict[str, Any]) -> bool: + """Return whether the classic title requests an explicit companion box.""" + title = payload.get("title") + return isinstance(title, dict) and _chart_companion_box(title) is not None + + def _chart_text_entry_font_size(item: dict[str, Any], fallback: int) -> int: raw = _first_present(item.get("font_size"), item.get("fontSize")) if raw is None: @@ -777,6 +783,16 @@ def _validate_chart_companion_boxes( include_subtitle_as_caption: bool, ) -> None: """Validate companion boxes without allocating shapes or relationships.""" + title_bounded = _chart_title_is_bounded(payload) + if ( + title_bounded + and not include_subtitle_as_caption + and _chart_text_entry(payload.get("subtitle")) is not None + ): + raise RuntimeError( + "Native PPTX classic chart bounded title does not support subtitle; " + "use a separately bounded caption" + ) _, chart_off_y, _, chart_ext_cy = chart_bounds below_index = 0 for item in _chart_companion_entries( @@ -808,6 +824,8 @@ def _chart_companion_text_xml( include_title: bool, include_subtitle_as_caption: bool, ) -> str: + if _chart_title_is_bounded(payload): + include_title = True entries = _chart_companion_entries( payload, include_title=include_title, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py index 8ca2a59a..bcc339b0 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py @@ -15,6 +15,7 @@ from .chart_data import ( _DEFAULT_CHART_COLORS, _category_axis_is_date, _chart_list, + _chart_plot_area_layout, _data_label_position, _data_label_point_items, _data_labels_config, @@ -30,6 +31,7 @@ from .chart_style import ( _chart_text_entry_font_size, _chart_text_entry, _chart_text_sizes, + _chart_title_is_bounded, _chart_tx_pr_xml, _classic_chart_style, _font_face_xml, @@ -495,11 +497,30 @@ def _chart_title_xml( return ( "" f"{''.join(paragraphs)}" - "" + "" '' ) +def _plot_area_layout_xml( + chart_data: dict[str, Any], + chart_bounds: tuple[int, int, int, int], +) -> str: + layout = _chart_plot_area_layout(chart_data, chart_bounds) + if layout is None: + return "" + x, y, width, height = layout + return ( + "" + '' + '' + '' + f'' + f'' + "" + ) + + def _chart_legend_xml( payload: dict[str, Any], *, @@ -1181,7 +1202,8 @@ def _chart_plot_xml( return ( '' f"{ser_xml}" - '' + '' + f'' "" ) if chart_type == "of_pie": @@ -1399,6 +1421,7 @@ def _chart_xml( *, chart_rels_id: str, chart_data: dict[str, Any], + chart_bounds: tuple[int, int, int, int], inherited_styles: dict[str, str] | None = None, primary_language: str | None = None, ) -> bytes: @@ -1420,11 +1443,12 @@ def _chart_xml( axis_titles=axis_titles, chart_style=chart_style, ) + bounded_title = _chart_title_is_bounded(payload) title_xml = _chart_title_xml( - payload.get("title"), + None if bounded_title else payload.get("title"), font_size=text_sizes["title"], color=chart_style.get("text_color"), - subtitle=payload.get("subtitle"), + subtitle=None if bounded_title else payload.get("subtitle"), subtitle_font_size=text_sizes["subtitle"], font_face=chart_style.get("font_face"), primary_language=primary_language, @@ -1451,7 +1475,7 @@ def _chart_xml( {title_xml} -{plot_xml}{_chart_area_sp_pr_xml(chart_style.get("plot_fill"))} +{_plot_area_layout_xml(chart_data, chart_bounds)}{plot_xml}{_chart_area_sp_pr_xml(chart_style.get("plot_fill"))} {legend_xml} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula.py new file mode 100644 index 00000000..b28d3f92 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +PPT Master - Native Formula Shape Builder + +Build editable PowerPoint formula shapes from explicit LaTeX markers. + +Usage: + Imported by the SVG-to-PPTX native-object converter. + +Examples: + from svg_to_pptx.native_objects.formula import build_native_formula + +Dependencies: + None (only uses standard library and local PPT Master modules) +""" + +from __future__ import annotations + +import math +import re +from dataclasses import dataclass +from typing import Any +from xml.etree import ElementTree as ET + +from ..drawingml.context import ConvertContext, ShapeResult +from ..drawingml.utils import _xml_escape, font_px_to_hpt +from .formula_compiler import FormulaCompileError, compile_latex_to_omml +from .marker_common import _bounds, _hex_or_none + + +DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" +A14_NS = "http://schemas.microsoft.com/office/drawing/2010/main" +MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006" +_MATH_RUN = f"{{{MATH_NS}}}r" +_MATH_RUN_PROPERTIES = f"{{{MATH_NS}}}rPr" +_DML_RUN_PROPERTIES = f"{{{DML_NS}}}rPr" +_LANGUAGE_RE = re.compile(r"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$") +_ALIGNMENTS = { + "center": ("ctr", "center"), + "left": ("l", "left"), + "right": ("r", "right"), +} + +for _prefix, _uri in (("a", DML_NS), ("m", MATH_NS)): + try: + ET.register_namespace(_prefix, _uri) + except (ValueError, AttributeError): + pass + + +@dataclass(frozen=True) +class FormulaSpec: + """Validated formula source plus the PowerPoint text style to apply.""" + + omml: str + font_size_hpt: int + color: str + paragraph_alignment: str + math_alignment: str + language: str + + +def _positive_number(value: Any, field_name: str) -> float: + if isinstance(value, bool): + raise RuntimeError(f"Native PPTX formula {field_name} must be numeric") + try: + result = float(value) + except (TypeError, ValueError, OverflowError) as exc: + raise RuntimeError( + f"Native PPTX formula {field_name} must be numeric" + ) from exc + if not math.isfinite(result) or result <= 0: + raise RuntimeError( + f"Native PPTX formula {field_name} must be a positive finite number" + ) + return result + + +def _formula_language(payload: dict[str, Any], ctx: ConvertContext | None) -> str: + raw = payload.get("language") + if raw is None and ctx is not None: + raw = ctx.primary_language + language = str(raw or "en-US").strip() + if len(language) > 35 or _LANGUAGE_RE.fullmatch(language) is None: + raise RuntimeError( + "Native PPTX formula language must be a compact BCP-47 tag" + ) + return language + + +def validate_formula_payload( + payload: dict[str, Any], + *, + ctx: ConvertContext | None = None, +) -> FormulaSpec: + """Validate and compile one block-formula payload.""" + latex = payload.get("latex") + if not isinstance(latex, str) or not latex.strip(): + raise RuntimeError("Native PPTX formula metadata requires non-empty `latex`") + + display = str(payload.get("display") or "block").strip().lower() + if display != "block": + raise RuntimeError( + "Native PPTX formula currently supports independent block formulas only" + ) + + raw_font_size = payload.get("font_size", 28) + font_size_px = _positive_number(raw_font_size, "font_size") + if font_size_px > 400: + raise RuntimeError("Native PPTX formula font_size must not exceed 400 px") + try: + font_size_hpt = font_px_to_hpt(font_size_px) + except ValueError as exc: + raise RuntimeError( + "Native PPTX formula font_size is outside the PowerPoint range" + ) from exc + + raw_color = payload.get("color", "#000000") + color = _hex_or_none(raw_color) + if color is None: + raise RuntimeError( + "Native PPTX formula color must be a visible CSS color or HEX value" + ) + + alignment = str(payload.get("align") or "center").strip().lower() + if alignment not in _ALIGNMENTS: + choices = ", ".join(sorted(_ALIGNMENTS)) + raise RuntimeError( + f"Native PPTX formula align must be one of: {choices}" + ) + paragraph_alignment, math_alignment = _ALIGNMENTS[alignment] + + try: + omml = compile_latex_to_omml(latex) + except FormulaCompileError as exc: + raise RuntimeError(f"Native PPTX formula LaTeX is unsupported: {exc}") from exc + + return FormulaSpec( + omml=omml, + font_size_hpt=font_size_hpt, + color=color, + paragraph_alignment=paragraph_alignment, + math_alignment=math_alignment, + language=_formula_language(payload, ctx), + ) + + +def _styled_omml(spec: FormulaSpec) -> str: + """Apply DrawingML run styling and alignment to validated OMML.""" + root = ET.fromstring(spec.omml) + if root.tag == f"{{{MATH_NS}}}oMathPara": + para_properties = root.find(f"{{{MATH_NS}}}oMathParaPr") + if para_properties is None: + para_properties = ET.Element(f"{{{MATH_NS}}}oMathParaPr") + root.insert(0, para_properties) + justification = para_properties.find(f"{{{MATH_NS}}}jc") + if justification is None: + justification = ET.SubElement( + para_properties, + f"{{{MATH_NS}}}jc", + ) + justification.set(f"{{{MATH_NS}}}val", spec.math_alignment) + + for run in root.iter(_MATH_RUN): + for existing in list(run): + if existing.tag == _DML_RUN_PROPERTIES: + run.remove(existing) + + run_properties = ET.Element( + _DML_RUN_PROPERTIES, + { + "lang": spec.language, + "sz": str(spec.font_size_hpt), + "dirty": "0", + }, + ) + solid_fill = ET.SubElement(run_properties, f"{{{DML_NS}}}solidFill") + ET.SubElement( + solid_fill, + f"{{{DML_NS}}}srgbClr", + {"val": spec.color}, + ) + for tag in ("latin", "ea", "cs"): + ET.SubElement( + run_properties, + f"{{{DML_NS}}}{tag}", + {"typeface": "Cambria Math"}, + ) + + insert_at = 0 + if len(run) and run[0].tag == _MATH_RUN_PROPERTIES: + insert_at = 1 + run.insert(insert_at, run_properties) + + return ET.tostring(root, encoding="unicode", short_empty_elements=True) + + +def build_native_formula( + elem: ET.Element, + ctx: ConvertContext, + payload: dict[str, Any], + spec: FormulaSpec | None = None, +) -> ShapeResult: + """Build one Choice-only editable formula with no image fallback.""" + formula_spec = spec or validate_formula_payload(payload, ctx=ctx) + off_x, off_y, ext_cx, ext_cy = _bounds(elem, payload, ctx) + shape_id = ctx.next_id() + name = _xml_escape( + str(payload.get("name") or elem.get("id") or f"Formula {shape_id}") + ) + omml = _styled_omml(formula_spec) + + xml = f''' + + + + + + + + + + + + + + + + + + + +{omml} + + + + +''' + return ShapeResult( + xml=xml, + bounds_emu=(off_x, off_y, off_x + ext_cx, off_y + ext_cy), + ) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula_compiler.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula_compiler.py new file mode 100644 index 00000000..d2a0589c --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/formula_compiler.py @@ -0,0 +1,1349 @@ +#!/usr/bin/env python3 +r""" +PPT Master - Native Formula Compiler + +Compile a strict LaTeX subset into editable block or inline Office Math XML. + +Usage: + Import compile_latex_to_omml() or compile_latex_to_inline_omml() from the + SVG-to-PPTX native-object pipeline. + +Examples: + compile_latex_to_omml(r"\frac{-b \pm \sqrt{b^2-4ac}}{2a}") + +Dependencies: + None (only uses standard library) +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from xml.etree import ElementTree as ET + + +MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" +DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +XML_NS = "http://www.w3.org/XML/1998/namespace" + +_MAX_LATEX_LENGTH = 65_536 +_MAX_OMML_LENGTH = 1_048_576 +_MAX_PARSE_DEPTH = 128 +_MAX_OMML_DEPTH = 256 +_MAX_AST_NODES = 100_000 +_FORBIDDEN_XML_RE = re.compile(r"±∓×÷·,;:!?") +_SPACING_COMMANDS = { + ",": "\u2009", + ":": "\u2005", + ";": "\u2004", + "!": "\u200b", + " ": "\u2005", +} + + +class _LatexParser: + """Parse the intentionally small formula-source grammar.""" + + def __init__(self, source: str) -> None: + self.source = source + self.position = 0 + self.depth = 0 + self.text_mode_depth = 0 + + def parse(self) -> _Sequence: + result = self._parse_sequence() + if self.position != len(self.source): + self._fail("Unexpected trailing formula source") + if _is_empty(result): + self._fail("Formula is empty") + return result + + def _parse_sequence( + self, + *, + terminator: str | None = None, + stop_at_right: bool = False, + stop_at_environment_boundary: bool = False, + ) -> _Sequence: + children: list[_Node] = [] + while self.position < len(self.source): + if terminator is not None and self.source[self.position] == terminator: + self.position += 1 + return _Sequence(tuple(children)) + if stop_at_right and self._at_control_word("right"): + return _Sequence(tuple(children)) + if stop_at_environment_boundary and ( + self.source[self.position] == "&" + or self.source.startswith("\\\\", self.position) + or self._at_control_word("end") + ): + return _Sequence(tuple(children)) + + char = self.source[self.position] + if char == "}": + self._fail("Unexpected closing group") + if char in "^_": + self._fail(f"Script marker {char!r} has no base") + if char == "&": + self._fail("Alignment marker '&' is only valid inside a matrix environment") + + atom = self._parse_complete_atom() + _append_child(children, atom) + + if terminator is not None: + self._fail(f"Unclosed group; expected {terminator!r}") + if stop_at_right: + self._fail("Unclosed delimiter; expected \\right") + if stop_at_environment_boundary: + self._fail("Unclosed matrix environment") + return _Sequence(tuple(children)) + + def _parse_atom(self) -> _Node: + char = self.source[self.position] + if char.isspace(): + while ( + self.position < len(self.source) + and self.source[self.position].isspace() + ): + self.position += 1 + return ( + _Text(" ", _TEXT_STYLE) + if self.text_mode_depth + else _Sequence(()) + ) + if char == "{": + return self._parse_group() + if char == "\\": + return self._parse_command() + if char in "$#%": + self._fail(f"Unsupported TeX syntax character {char!r}") + if ord(char) < 32: + self._fail("Unsupported control character") + + self.position += 1 + if char == "-": + char = "−" + elif char == "~": + return _Text("\u2005", _SPACING_STYLE) + style = _ROMAN_STYLE if char in _PLAIN_OPERATOR_CHARS else None + return _Text(char, style) + + def _parse_command(self) -> _Node: + command_start = self.position + self.position += 1 + if self.position >= len(self.source): + self._fail("Trailing backslash", position=command_start) + + char = self.source[self.position] + if not _is_command_letter(char): + self.position += 1 + if char == "\\": + self._fail( + "Row separator '\\\\' is only valid inside a matrix environment", + position=command_start, + ) + escaped = { + "{": "{", + "}": "}", + "_": "_", + "%": "%", + "#": "#", + "$": "$", + "&": "&", + "|": "‖", + } + if char in escaped: + return _Text(escaped[char]) + if char in _SPACING_COMMANDS: + return _Text(_SPACING_COMMANDS[char], _SPACING_STYLE) + self._fail(f"Unsupported control symbol \\{char}", position=command_start) + + command = self._read_control_word_body() + self._skip_whitespace() + + if command in _GREEK_SYMBOLS: + return _Text(_GREEK_SYMBOLS[command]) + if command in _UPRIGHT_SYMBOLS: + return _Text(_UPRIGHT_SYMBOLS[command], _ROMAN_STYLE) + if command in _NAMED_OPERATORS: + return _Text(command, _ROMAN_STYLE) + if command in _DELIMITER_COMMANDS: + return _Text(_DELIMITER_COMMANDS[command], _ROMAN_STYLE) + if command in _NARY_SYMBOLS: + symbol, limit_location = _NARY_SYMBOLS[command] + return _Nary(symbol=symbol, limit_location=limit_location) + if command == "frac": + numerator = self._parse_required_group("fraction numerator") + denominator = self._parse_required_group("fraction denominator") + return _Fraction(numerator=numerator, denominator=denominator) + if command == "sqrt": + degree = self._parse_optional_degree() + body = self._parse_required_group("radical body") + return _Radical(body=body, degree=degree) + if command == "left": + return self._parse_delimited_expression() + if command == "right": + self._fail("Unexpected \\right without matching \\left", position=command_start) + if command == "begin": + return self._parse_environment() + if command == "end": + self._fail("Unexpected \\end without matching \\begin", position=command_start) + if command in {"text", "mathrm", "mathbf", "mathit", "mathbb", "mathcal"}: + styles = { + "text": _TEXT_STYLE, + "mathrm": _ROMAN_STYLE, + "mathbf": _RunStyle(style="b"), + "mathit": _RunStyle(style="i"), + "mathbb": _RunStyle(style="p", script="double-struck"), + "mathcal": _RunStyle(style="p", script="script"), + } + if command == "text": + self.text_mode_depth += 1 + try: + body = self._parse_required_group(f"\\{command} body") + finally: + if command == "text": + self.text_mode_depth -= 1 + return _Styled(body=body, style=styles[command]) + if command in _ACCENT_COMMANDS: + body = self._parse_required_group(f"\\{command} body") + return _Accent(character=_ACCENT_COMMANDS[command], body=body) + if command == "quad": + return _Text("\u2001", _SPACING_STYLE) + if command == "qquad": + return _Text("\u2001\u2001", _SPACING_STYLE) + + self._fail(f"Unsupported LaTeX command \\{command}", position=command_start) + + def _parse_group(self) -> _Sequence: + self.position += 1 + return self._parse_nested_sequence(terminator="}") + + def _parse_required_group(self, description: str) -> _Sequence: + self._skip_whitespace() + if self.position >= len(self.source) or self.source[self.position] != "{": + self._fail(f"{description.capitalize()} must use a braced group") + result = self._parse_group() + if _is_empty(result): + self._fail(f"{description.capitalize()} cannot be empty") + return result + + def _parse_optional_degree(self) -> _Sequence | None: + if self.position >= len(self.source) or self.source[self.position] != "[": + return None + self.position += 1 + result = self._parse_nested_sequence(terminator="]") + if _is_empty(result): + self._fail("Radical degree cannot be empty") + return result + + def _parse_nested_sequence(self, **kwargs: object) -> _Sequence: + self.depth += 1 + if self.depth > _MAX_PARSE_DEPTH: + self._fail(f"Formula nesting exceeds {_MAX_PARSE_DEPTH} levels") + try: + return self._parse_sequence(**kwargs) + finally: + self.depth -= 1 + + def _parse_scripts(self, base: _Node) -> _Node: + subscript: _Sequence | None = None + superscript: _Sequence | None = None + while True: + saved_position = self.position + self._skip_whitespace() + if ( + self.position >= len(self.source) + or self.source[self.position] not in "^_" + ): + self.position = saved_position + break + + marker = self.source[self.position] + self.position += 1 + self._skip_whitespace() + argument = self._parse_script_argument(marker) + if marker == "_": + if subscript is not None: + self._fail("Duplicate subscript for one base") + subscript = argument + else: + if superscript is not None: + self._fail("Duplicate superscript for one base") + superscript = argument + + if subscript is None and superscript is None: + return base + if isinstance(base, _Nary): + return _Nary( + symbol=base.symbol, + limit_location=base.limit_location, + subscript=subscript, + superscript=superscript, + body=base.body, + limit_modifier=base.limit_modifier, + ) + return _Script(base=base, subscript=subscript, superscript=superscript) + + def _parse_complete_atom(self) -> _Node: + atom = self._parse_atom() + if isinstance(atom, _Nary): + atom = self._parse_nary_limit_modifier(atom) + atom = self._parse_scripts(atom) + if isinstance(atom, _Nary): + atom = self._parse_nary_limit_modifier(atom) + atom = self._parse_nary_body(atom) + return atom + + def _parse_nary_limit_modifier(self, node: _Nary) -> _Nary: + saved_position = self.position + self._skip_whitespace() + if self._at_control_word("limits"): + limit_location = "undOvr" + command = "limits" + elif self._at_control_word("nolimits"): + limit_location = "subSup" + command = "nolimits" + else: + self.position = saved_position + return node + + if node.limit_modifier is not None: + self._fail("N-ary operator has more than one limit modifier") + self._consume_control_word(command) + return _Nary( + symbol=node.symbol, + limit_location=limit_location, + subscript=node.subscript, + superscript=node.superscript, + body=node.body, + limit_modifier=command, + ) + + def _parse_nary_body(self, node: _Nary) -> _Nary: + saved_position = self.position + spacing = self._parse_nary_spacing() + if not self._nary_body_follows(): + self.position = saved_position + return node + + if self.source[self.position] == "{": + operand = self._parse_group() + else: + operand = _Sequence((self._parse_nested_atom(),)) + body = _Sequence((*spacing, *operand.children)) + if _is_empty(body): + self._fail("N-ary operator body cannot be empty") + return _Nary( + symbol=node.symbol, + limit_location=node.limit_location, + subscript=node.subscript, + superscript=node.superscript, + body=body, + limit_modifier=node.limit_modifier, + ) + + def _parse_nested_atom(self) -> _Node: + """Parse one recursively owned atom under the shared depth limit.""" + self.depth += 1 + if self.depth > _MAX_PARSE_DEPTH: + self._fail(f"Formula nesting exceeds {_MAX_PARSE_DEPTH} levels") + try: + return self._parse_complete_atom() + finally: + self.depth -= 1 + + def _parse_nary_spacing(self) -> tuple[_Node, ...]: + """Consume spacing before a n-ary operand without losing explicit TeX.""" + spacing: list[_Node] = [] + while True: + self._skip_whitespace() + if self._at_control_word("quad"): + self._consume_control_word("quad") + spacing.append(_Text("\u2001", _SPACING_STYLE)) + continue + if self._at_control_word("qquad"): + self._consume_control_word("qquad") + spacing.append(_Text("\u2001\u2001", _SPACING_STYLE)) + continue + if ( + self.position + 1 < len(self.source) + and self.source[self.position] == "\\" + and self.source[self.position + 1] in {",", ";", ":", "!", " "} + ): + command = self.source[self.position + 1] + self.position += 2 + spacing.append(_Text(_SPACING_COMMANDS[command], _SPACING_STYLE)) + continue + return tuple(spacing) + + def _nary_body_follows(self) -> bool: + if self.position >= len(self.source): + return False + if self.source.startswith("\\\\", self.position): + return False + char = self.source[self.position] + if char in "}])&^_+-=<>/,;:!?": + return False + if char != "\\": + return True + if self._at_control_word("right") or self._at_control_word("end"): + return False + command_position = self.position + 1 + command_end = command_position + while ( + command_end < len(self.source) + and _is_command_letter(self.source[command_end]) + ): + command_end += 1 + command = self.source[command_position:command_end] + return command not in _NARY_BODY_STOP_COMMANDS + + def _parse_script_argument(self, marker: str) -> _Sequence: + if self.position >= len(self.source): + self._fail(f"Script marker {marker!r} has no argument") + if self.source[self.position] == "{": + result = self._parse_group() + else: + result = _Sequence((self._parse_atom(),)) + if _is_empty(result): + self._fail(f"Script marker {marker!r} has an empty argument") + return result + + def _parse_delimited_expression(self) -> _Delimiter: + left = self._parse_delimiter_token("\\left") + body = self._parse_nested_sequence(stop_at_right=True) + self._consume_control_word("right") + right = self._parse_delimiter_token("\\right") + return _Delimiter(left=left, right=right, body=body) + + def _parse_delimiter_token(self, owner: str) -> str: + self._skip_whitespace() + if self.position >= len(self.source): + self._fail(f"{owner} requires a delimiter") + + char = self.source[self.position] + if char != "\\": + self.position += 1 + if char in "()[]|.": + return "" if char == "." else char + self._fail(f"Unsupported delimiter {char!r} after {owner}") + + command_start = self.position + self.position += 1 + if self.position >= len(self.source): + self._fail(f"{owner} requires a delimiter", position=command_start) + if not _is_command_letter(self.source[self.position]): + delimiter = self.source[self.position] + self.position += 1 + if delimiter in "{}|": + return "‖" if delimiter == "|" else delimiter + self._fail(f"Unsupported delimiter command \\{delimiter}", position=command_start) + + command = self._read_control_word_body() + self._skip_whitespace() + if command not in _DELIMITER_COMMANDS: + self._fail(f"Unsupported delimiter command \\{command}", position=command_start) + return _DELIMITER_COMMANDS[command] + + def _parse_environment(self) -> _Matrix: + environment = self._parse_environment_name("\\begin") + if environment not in _ENVIRONMENT_DELIMITERS: + self._fail(f"Unsupported formula environment {environment!r}") + + rows: list[tuple[_Sequence, ...]] = [] + row: list[_Sequence] = [] + while True: + cell = self._parse_nested_sequence(stop_at_environment_boundary=True) + if _is_empty(cell): + self._fail(f"Environment {environment!r} contains an empty cell") + row.append(cell) + + if self.source[self.position] == "&": + self.position += 1 + continue + if self.source.startswith("\\\\", self.position): + self.position += 2 + rows.append(tuple(row)) + row = [] + self._skip_whitespace() + if self._at_control_word("end"): + self._consume_environment_end(environment) + break + continue + if self._at_control_word("end"): + rows.append(tuple(row)) + self._consume_environment_end(environment) + break + self._fail(f"Malformed environment {environment!r}") + + column_count = len(rows[0]) + if any(len(current_row) != column_count for current_row in rows): + self._fail(f"Environment {environment!r} has inconsistent column counts") + return _Matrix(environment=environment, rows=tuple(rows)) + + def _consume_environment_end(self, expected: str) -> None: + self._consume_control_word("end") + actual = self._parse_environment_name("\\end") + if actual != expected: + self._fail( + f"Mismatched environment ending: expected {expected!r}, got {actual!r}" + ) + + def _parse_environment_name(self, owner: str) -> str: + if self.position >= len(self.source) or self.source[self.position] != "{": + self._fail(f"{owner} requires a braced environment name") + closing = self.source.find("}", self.position + 1) + if closing < 0: + self._fail(f"Unclosed environment name after {owner}") + name = self.source[self.position + 1 : closing] + if not _ENVIRONMENT_NAME_RE.fullmatch(name): + self._fail(f"Invalid environment name {name!r}") + self.position = closing + 1 + return name + + def _at_control_word(self, expected: str) -> bool: + prefix = f"\\{expected}" + if not self.source.startswith(prefix, self.position): + return False + following = self.position + len(prefix) + return ( + following >= len(self.source) + or not _is_command_letter(self.source[following]) + ) + + def _consume_control_word(self, expected: str) -> None: + if not self._at_control_word(expected): + self._fail(f"Expected \\{expected}") + self.position += len(expected) + 1 + self._skip_whitespace() + + def _read_control_word_body(self) -> str: + start = self.position + while ( + self.position < len(self.source) + and _is_command_letter(self.source[self.position]) + ): + self.position += 1 + return self.source[start : self.position] + + def _skip_whitespace(self) -> None: + while ( + self.position < len(self.source) + and self.source[self.position].isspace() + ): + self.position += 1 + + def _fail(self, message: str, *, position: int | None = None) -> None: + offset = self.position if position is None else position + start = max(0, offset - 12) + end = min(len(self.source), offset + 12) + context = self.source[start:end].replace("\n", " ") + raise FormulaCompileError(f"{message} at offset {offset}: {context!r}") + + +def _is_command_letter(char: str) -> bool: + return char.isascii() and char.isalpha() + + +def _xml_character_allowed(char: str) -> bool: + codepoint = ord(char) + return ( + codepoint in {0x09, 0x0A, 0x0D} + or 0x20 <= codepoint <= 0xD7FF + or 0xE000 <= codepoint <= 0xFFFD + or 0x10000 <= codepoint <= 0x10FFFF + ) + + +def _append_child(children: list[_Node], node: _Node) -> None: + if isinstance(node, _Sequence) and not node.children: + return + if ( + isinstance(node, _Text) + and children + and isinstance(children[-1], _Text) + and children[-1].style == node.style + ): + previous = children[-1] + children[-1] = _Text(previous.value + node.value, node.style) + return + children.append(node) + + +def _is_empty(node: _Node) -> bool: + if isinstance(node, _Text): + return not node.value + if isinstance(node, _Sequence): + return not node.children or all(_is_empty(child) for child in node.children) + if isinstance(node, _Styled): + return _is_empty(node.body) + return False + + +def _formula_node_count(root: _Node) -> int: + """Return iterative AST size and reject impractically complex formulas.""" + pending: list[_Node] = [root] + count = 0 + while pending: + node = pending.pop() + count += 1 + if count > _MAX_AST_NODES: + raise FormulaCompileError( + f"Formula exceeds the {_MAX_AST_NODES}-node complexity limit" + ) + if isinstance(node, _Sequence): + pending.extend(reversed(node.children)) + elif isinstance(node, _Styled): + pending.append(node.body) + elif isinstance(node, _Fraction): + pending.extend((node.denominator, node.numerator)) + elif isinstance(node, _Radical): + pending.append(node.body) + if node.degree is not None: + pending.append(node.degree) + elif isinstance(node, _Script): + pending.append(node.base) + if node.subscript is not None: + pending.append(node.subscript) + if node.superscript is not None: + pending.append(node.superscript) + elif isinstance(node, _Nary): + if node.subscript is not None: + pending.append(node.subscript) + if node.superscript is not None: + pending.append(node.superscript) + if node.body is not None: + pending.append(node.body) + elif isinstance(node, _Delimiter): + pending.append(node.body) + elif isinstance(node, _Matrix): + for row in reversed(node.rows): + pending.extend(reversed(row)) + elif isinstance(node, _Accent): + pending.append(node.body) + return count + + +def _math_tag(local_name: str) -> str: + return f"{{{MATH_NS}}}{local_name}" + + +def _math_attrs(**values: str) -> dict[str, str]: + return {_math_tag(name): value for name, value in values.items()} + + +def _math_element( + parent: ET.Element, + local_name: str, + **attributes: str, +) -> ET.Element: + return ET.SubElement(parent, _math_tag(local_name), _math_attrs(**attributes)) + + +def _append_run(parent: ET.Element, value: str, style: _RunStyle | None) -> None: + if not value: + return + run = _math_element(parent, "r") + if style is not None and (style.style or style.normal or style.script): + properties = _math_element(run, "rPr") + if style.normal: + _math_element(properties, "nor", val="on") + if style.style: + _math_element(properties, "sty", val=style.style) + if style.script: + _math_element(properties, "scr", val=style.script) + text = _math_element(run, "t") + if value != value.strip() or " " in value: + text.set(f"{{{XML_NS}}}space", "preserve") + text.text = value + + +def _append_node( + parent: ET.Element, + node: _Node, + inherited_style: _RunStyle | None = None, +) -> None: + if isinstance(node, _Text): + _append_run(parent, node.value, node.style or inherited_style) + return + if isinstance(node, _Sequence): + for child in node.children: + _append_node(parent, child, inherited_style) + return + if isinstance(node, _Styled): + _append_node(parent, node.body, node.style) + return + if isinstance(node, _Fraction): + fraction = _math_element(parent, "f") + properties = _math_element(fraction, "fPr") + _math_element(properties, "type", val="bar") + numerator = _math_element(fraction, "num") + _append_node(numerator, node.numerator, inherited_style) + denominator = _math_element(fraction, "den") + _append_node(denominator, node.denominator, inherited_style) + return + if isinstance(node, _Radical): + radical = _math_element(parent, "rad") + properties = _math_element(radical, "radPr") + _math_element(properties, "degHide", val="off" if node.degree else "on") + degree = _math_element(radical, "deg") + if node.degree is not None: + _append_node(degree, node.degree, inherited_style) + body = _math_element(radical, "e") + _append_node(body, node.body, inherited_style) + return + if isinstance(node, _Script): + _append_script(parent, node, inherited_style) + return + if isinstance(node, _Nary): + _append_nary(parent, node, inherited_style) + return + if isinstance(node, _Delimiter): + delimiter = _math_element(parent, "d") + properties = _math_element(delimiter, "dPr") + _math_element(properties, "begChr", val=node.left) + _math_element(properties, "endChr", val=node.right) + _math_element(properties, "sepChr", val="") + _math_element(properties, "grow") + body = _math_element(delimiter, "e") + _append_node(body, node.body, inherited_style) + return + if isinstance(node, _Matrix): + _append_matrix(parent, node, inherited_style) + return + if isinstance(node, _Accent): + accent = _math_element(parent, "acc") + properties = _math_element(accent, "accPr") + _math_element(properties, "chr", val=node.character) + body = _math_element(accent, "e") + _append_node(body, node.body, inherited_style) + return + raise FormulaCompileError(f"Unsupported internal formula node: {type(node).__name__}") + + +def _append_script( + parent: ET.Element, + node: _Script, + inherited_style: _RunStyle | None, +) -> None: + if node.subscript is not None and node.superscript is not None: + script = _math_element(parent, "sSubSup") + elif node.subscript is not None: + script = _math_element(parent, "sSub") + else: + script = _math_element(parent, "sSup") + + base = _math_element(script, "e") + _append_node(base, node.base, inherited_style) + if node.subscript is not None: + subscript = _math_element(script, "sub") + _append_node(subscript, node.subscript, inherited_style) + if node.superscript is not None: + superscript = _math_element(script, "sup") + _append_node(superscript, node.superscript, inherited_style) + + +def _append_nary( + parent: ET.Element, + node: _Nary, + inherited_style: _RunStyle | None, +) -> None: + nary = _math_element(parent, "nary") + properties = _math_element(nary, "naryPr") + _math_element(properties, "chr", val=node.symbol) + _math_element(properties, "limLoc", val=node.limit_location) + _math_element(properties, "subHide", val="off" if node.subscript else "on") + _math_element(properties, "supHide", val="off" if node.superscript else "on") + + subscript = _math_element(nary, "sub") + if node.subscript is not None: + _append_node(subscript, node.subscript, inherited_style) + superscript = _math_element(nary, "sup") + if node.superscript is not None: + _append_node(superscript, node.superscript, inherited_style) + body = _math_element(nary, "e") + if node.body is not None: + _append_node(body, node.body, inherited_style) + + +def _append_matrix( + parent: ET.Element, + node: _Matrix, + inherited_style: _RunStyle | None, +) -> None: + delimiters = _ENVIRONMENT_DELIMITERS[node.environment] + matrix_parent = parent + if delimiters is not None: + delimiter = _math_element(parent, "d") + properties = _math_element(delimiter, "dPr") + _math_element(properties, "begChr", val=delimiters[0]) + _math_element(properties, "endChr", val=delimiters[1]) + _math_element(properties, "sepChr", val="") + _math_element(properties, "grow") + matrix_parent = _math_element(delimiter, "e") + + matrix = _math_element(matrix_parent, "m") + properties = _math_element(matrix, "mPr") + _math_element(properties, "baseJc", val="center") + _math_element(properties, "plcHide", val="on") + columns = _math_element(properties, "mcs") + for index in range(len(node.rows[0])): + column = _math_element(columns, "mc") + column_properties = _math_element(column, "mcPr") + justification = "center" + if node.environment == "cases": + justification = "left" + elif node.environment == "aligned": + justification = "right" if index % 2 == 0 else "left" + _math_element(column_properties, "mcJc", val=justification) + _math_element(column_properties, "count", val="1") + + for row_data in node.rows: + row = _math_element(matrix, "mr") + for cell_data in row_data: + cell = _math_element(row, "e") + _append_node(cell, cell_data, inherited_style) + + +def _qualified_name(name: str) -> tuple[str | None, str]: + if name.startswith("{") and "}" in name: + namespace, local_name = name[1:].split("}", 1) + return namespace, local_name + return None, name + + +def _validate_xml_tree(root: ET.Element) -> None: + namespace, local_name = _qualified_name(root.tag) + if namespace != MATH_NS or local_name not in {"oMathPara", "oMath"}: + raise FormulaCompileError("OMML root must be m:oMathPara or m:oMath") + + math_roots = 0 + visible_text: list[str] = [] + pending: list[tuple[ET.Element, int]] = [(root, 1)] + while pending: + element, depth = pending.pop() + if depth > _MAX_OMML_DEPTH: + raise FormulaCompileError( + f"OMML nesting exceeds {_MAX_OMML_DEPTH} levels" + ) + pending.extend((child, depth + 1) for child in reversed(element)) + element_namespace, element_name = _qualified_name(element.tag) + if ( + element_namespace == MATH_NS + and element_name not in _MATH_ELEMENTS + ): + raise FormulaCompileError( + f"OMML contains unsupported math element: {element_name!r}" + ) + if ( + element_namespace == DRAWING_NS + and element_name not in _DRAWING_ELEMENTS + ): + raise FormulaCompileError( + f"OMML contains unsupported DrawingML element: {element_name!r}" + ) + if element_namespace not in {MATH_NS, DRAWING_NS}: + raise FormulaCompileError( + f"OMML contains unsupported element namespace: {element_namespace!r}" + ) + if element_namespace == MATH_NS and element_name in {"oMathPara", "oMath"}: + math_roots += 1 + for attribute in element.attrib: + attribute_namespace, attribute_name = _qualified_name(attribute) + if ( + attribute_namespace == MATH_NS + and attribute_name in _MATH_VALUE_ATTRIBUTES + ): + continue + if ( + attribute_namespace in {None, DRAWING_NS} + and element_namespace == DRAWING_NS + and attribute_name in _DRAWING_ATTRIBUTES + ): + continue + if ( + attribute_namespace == XML_NS + and element_namespace == MATH_NS + and element_name == "t" + and attribute_name == "space" + and element.attrib[attribute] == "preserve" + ): + continue + if attribute_namespace is not None or attribute_name: + raise FormulaCompileError( + f"OMML contains unsupported attribute {attribute_name!r}" + ) + if element.text: + if element_namespace == MATH_NS and element_name == "t": + visible_text.append(element.text) + elif element.text.strip(): + raise FormulaCompileError( + f"OMML text is only allowed inside m:t, found in {element_name!r}" + ) + if element.tail and element.tail.strip(): + raise FormulaCompileError("OMML contains unexpected trailing text") + + expected_roots = 2 if local_name == "oMathPara" else 1 + if math_roots != expected_roots: + raise FormulaCompileError("OMML must contain exactly one math expression") + if _LATEX_COMMAND_RE.search("".join(visible_text)): + raise FormulaCompileError("OMML contains an uncompiled LaTeX command") + + if local_name == "oMathPara": + direct_expressions = [ + child + for child in root + if _qualified_name(child.tag) == (MATH_NS, "oMath") + ] + if len(direct_expressions) != 1: + raise FormulaCompileError( + "m:oMathPara must contain exactly one direct m:oMath expression" + ) + + +def validate_omml_fragment(xml: str) -> str: + """Validate the supported Office Math subset and canonicalize prefixes.""" + if not isinstance(xml, str): + raise FormulaCompileError("OMML fragment must be a string") + if not xml.strip(): + raise FormulaCompileError("OMML fragment is empty") + if len(xml) > _MAX_OMML_LENGTH: + raise FormulaCompileError( + f"OMML fragment exceeds the {_MAX_OMML_LENGTH}-character limit" + ) + if _FORBIDDEN_XML_RE.search(xml): + raise FormulaCompileError("DOCTYPE and ENTITY declarations are forbidden in OMML") + + try: + root = ET.fromstring(xml) + except (ET.ParseError, RecursionError) as exc: + raise FormulaCompileError(f"Invalid OMML XML: {exc}") from exc + _validate_xml_tree(root) + + ET.register_namespace("m", MATH_NS) + ET.register_namespace("a", DRAWING_NS) + try: + canonical = ET.tostring( + root, + encoding="unicode", + short_empty_elements=True, + ) + except RecursionError as exc: + raise FormulaCompileError( + "OMML nesting exceeds the XML serializer limit" + ) from exc + if len(canonical) > _MAX_OMML_LENGTH: + raise FormulaCompileError( + f"Canonical OMML exceeds the {_MAX_OMML_LENGTH}-character limit" + ) + return canonical + + +def compile_latex_to_omml(latex: str) -> str: + """Compile one standalone block formula into canonical m:oMathPara XML.""" + expression = _parse_latex_formula(latex) + + root = ET.Element(_math_tag("oMathPara")) + properties = _math_element(root, "oMathParaPr") + _math_element(properties, "jc", val="center") + math = _math_element(root, "oMath") + _append_node(math, expression) + + ET.register_namespace("m", MATH_NS) + xml = ET.tostring(root, encoding="unicode", short_empty_elements=True) + return validate_omml_fragment(xml) + + +def compile_latex_to_inline_omml(latex: str) -> str: + """Compile one inline formula into canonical m:oMath XML.""" + expression = _parse_latex_formula(latex) + + root = ET.Element(_math_tag("oMath")) + _append_node(root, expression) + + ET.register_namespace("m", MATH_NS) + xml = ET.tostring(root, encoding="unicode", short_empty_elements=True) + return validate_omml_fragment(xml) + + +def _parse_latex_formula(latex: str) -> _Node: + """Validate and parse the shared formula-source contract once.""" + if not isinstance(latex, str): + raise FormulaCompileError("LaTeX formula must be a string") + if len(latex) > _MAX_LATEX_LENGTH: + raise FormulaCompileError( + f"LaTeX formula exceeds the {_MAX_LATEX_LENGTH}-character limit" + ) + source = latex.strip() + if not source: + raise FormulaCompileError("LaTeX formula is empty") + if any(not _xml_character_allowed(char) for char in source): + raise FormulaCompileError( + "LaTeX formula contains an invalid XML character" + ) + + expression = _LatexParser(source).parse() + _formula_node_count(expression) + return expression + + +__all__ = [ + "FormulaCompileError", + "compile_latex_to_inline_omml", + "compile_latex_to_omml", + "validate_omml_fragment", +] diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/inline_formula.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/inline_formula.py new file mode 100644 index 00000000..cb2d28d4 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/inline_formula.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +""" +PPT Master - Native Inline Formula Contract + +Validate SVG inline-formula markers and build editable PowerPoint math runs. + +Usage: + Imported by the SVG quality checker and SVG-to-PPTX text converter. + +Examples: + xᵢ² + +Dependencies: + None (only uses standard library and local PPT Master modules) +""" + +from __future__ import annotations + +from copy import deepcopy +from xml.etree import ElementTree as ET + +from ..drawingml.utils import parse_inline_style, parse_svg_color +from .formula_compiler import ( + FormulaCompileError, + compile_latex_to_inline_omml, +) + + +SVG_NS = "http://www.w3.org/2000/svg" +DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +OFFICE_REL_NS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +) +MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" +A14_NS = "http://schemas.microsoft.com/office/drawing/2010/main" +MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006" +INLINE_FORMULA_ATTR = "data-pptx-inline-formula" +_SVG_TEXT = f"{{{SVG_NS}}}text" +_SVG_TSPAN = f"{{{SVG_NS}}}tspan" +_SVG_A = f"{{{SVG_NS}}}a" +_MATH_RUN = f"{{{MATH_NS}}}r" +_MATH_RUN_PROPERTIES = f"{{{MATH_NS}}}rPr" +_DML_RUN_PROPERTIES = f"{{{DML_NS}}}rPr" +_POSITION_ATTRIBUTES = ("x", "y", "dx", "dy") +_PARAGRAPH_ATTRIBUTES = ( + "data-paragraph-line-height", + "data-paragraph-line-break", + "data-paragraph-soft-break", + "data-paragraph-space-before", +) +_NON_OUTPUT_ANCESTORS = frozenset({ + "clipPath", + "desc", + "defs", + "filter", + "linearGradient", + "marker", + "mask", + "metadata", + "pattern", + "radialGradient", + "style", + "symbol", + "title", +}) + +for _prefix, _uri in (("a", DML_NS), ("m", MATH_NS)): + try: + ET.register_namespace(_prefix, _uri) + except (ValueError, AttributeError): + pass + + +def _marker_label(elem: ET.Element) -> str: + elem_id = (elem.get("id") or "").strip() + return f"" if elem_id else "" + + +def inline_formula_marker_errors(root: ET.Element) -> list[str]: + """Return strict authoring errors for every inline-formula marker.""" + errors: list[str] = [] + parent_map = {child: parent for parent in root.iter() for child in parent} + markers = [ + elem for elem in root.iter() + if elem.get(INLINE_FORMULA_ATTR) is not None + ] + + for marker in markers: + label = _marker_label(marker) + if marker.tag != _SVG_TSPAN: + tag = marker.tag.rsplit("}", 1)[-1] + errors.append( + f"{INLINE_FORMULA_ATTR} is only valid on , found <{tag}>" + ) + continue + + if marker.get("data-pptx-replace-with") is not None: + errors.append( + f"{label} cannot also declare data-pptx-replace-with" + ) + + source = marker.get(INLINE_FORMULA_ATTR) or "" + if not source.strip(): + errors.append(f"{label} requires non-empty {INLINE_FORMULA_ATTR}") + else: + try: + compile_latex_to_inline_omml(source) + except FormulaCompileError as exc: + errors.append(f"{label} has unsupported inline LaTeX: {exc}") + + if list(marker): + errors.append( + f"{label} must contain preview text directly and cannot nest elements" + ) + if not (marker.text or "").strip(): + errors.append(f"{label} requires non-empty visible SVG preview text") + elif marker.text != marker.text.strip(): + errors.append( + f"{label} preview cannot contain leading or trailing whitespace; " + "place spacing in the surrounding text" + ) + + positioned = [name for name in _POSITION_ATTRIBUTES if marker.get(name) is not None] + if positioned: + errors.append( + f"{label} is an inline run and cannot set " + ", ".join(positioned) + ) + paragraph_attrs = [ + name for name in _PARAGRAPH_ATTRIBUTES + if marker.get(name) is not None + ] + if paragraph_attrs: + errors.append( + f"{label} cannot own paragraph layout metadata: " + + ", ".join(paragraph_attrs) + ) + + effective_fill: str | None = None + style_owner: ET.Element | None = marker + while style_owner is not None: + inline_style = parse_inline_style(style_owner.get("style")) + effective_fill = inline_style.get("fill") + if effective_fill is None: + effective_fill = style_owner.get("fill") + if effective_fill is not None: + break + style_owner = parent_map.get(style_owner) + color, alpha = parse_svg_color(effective_fill or "#000000") + if color is None or alpha <= 0: + errors.append( + f"{label} requires one visible solid fill color inherited " + "from itself or its text ancestors" + ) + + parent = parent_map.get(marker) + text_owner: ET.Element | None = None + nested_marker = False + inside_block_formula = False + inside_native_replacement = False + inside_preserved_text = False + inside_placeholder = False + inside_skipped_transport = False + invalid_inline_container: str | None = None + non_output_ancestor: str | None = None + fixed_structure_layer: str | None = None + while parent is not None: + parent_tag = parent.tag.rsplit("}", 1)[-1] + if text_owner is None and parent.tag == _SVG_TEXT: + text_owner = parent + elif ( + text_owner is None + and parent.tag not in {_SVG_A, _SVG_TSPAN} + and invalid_inline_container is None + ): + invalid_inline_container = parent_tag + if parent_tag in _NON_OUTPUT_ANCESTORS: + non_output_ancestor = parent_tag + if parent.get(INLINE_FORMULA_ATTR) is not None: + nested_marker = True + replacement = ( + parent.get("data-pptx-replace-with") or "" + ).strip().lower() + if replacement: + inside_native_replacement = True + inside_block_formula = ( + inside_block_formula or replacement == "formula" + ) + if (parent.get("data-pptx-part") or "").strip() in { + "geometry-detail", + "geometry-preview", + }: + inside_skipped_transport = True + if parent.get("data-pptx-placeholder") is not None: + inside_placeholder = True + layer = (parent.get("data-pptx-layer") or "").strip().lower() + if layer in {"master", "layout"}: + fixed_structure_layer = layer + if any( + child.tag.rsplit("}", 1)[-1] == "metadata" + and child.get("data-pptx-part") == "txbody" + for child in parent + ): + inside_preserved_text = True + parent = parent_map.get(parent) + if text_owner is None: + errors.append(f"{label} must be inside an SVG element") + elif invalid_inline_container is not None: + errors.append( + f"{label} can only be nested through / elements before " + f"its owning , found <{invalid_inline_container}>" + ) + if non_output_ancestor is not None: + errors.append( + f"{label} cannot be placed inside non-output " + f"<{non_output_ancestor}> content" + ) + if nested_marker: + errors.append(f"{label} cannot be nested inside another inline formula marker") + if inside_block_formula: + errors.append( + f"{label} cannot be placed inside a block formula preview" + ) + elif inside_native_replacement: + errors.append( + f"{label} cannot be placed inside a native replacement subtree" + ) + if inside_skipped_transport: + errors.append( + f"{label} cannot be placed inside non-output geometry transport" + ) + if inside_preserved_text: + errors.append( + f"{label} cannot be placed inside an imported preserved txBody group" + ) + if inside_placeholder: + errors.append( + f"{label} cannot be used inside a structured Layout placeholder" + ) + if fixed_structure_layer is not None: + errors.append( + f"{label} cannot be used on the {fixed_structure_layer} layer; " + "inline formulas are slide-local text" + ) + + return errors + + +def _apply_run_properties(omml: str, run_properties_xml: str) -> str: + """Replace DrawingML properties on every Office Math leaf run.""" + try: + root = ET.fromstring(omml) + wrapper = ET.fromstring( + f'' + f"{run_properties_xml}" + ) + except (ET.ParseError, RecursionError) as exc: + raise RuntimeError(f"Invalid inline formula XML: {exc}") from exc + if len(wrapper) != 1: + raise RuntimeError("Inline formula styling must contain one a:rPr root") + run_properties = wrapper[0] + if root.tag != f"{{{MATH_NS}}}oMath": + raise RuntimeError("Inline formula compiler must return one m:oMath root") + if run_properties.tag != _DML_RUN_PROPERTIES: + raise RuntimeError("Inline formula styling must use one a:rPr root") + + for run in root.iter(_MATH_RUN): + for existing in list(run): + if existing.tag == _DML_RUN_PROPERTIES: + run.remove(existing) + insert_at = 1 if len(run) and run[0].tag == _MATH_RUN_PROPERTIES else 0 + run.insert(insert_at, deepcopy(run_properties)) + + return ET.tostring(root, encoding="unicode", short_empty_elements=True) + + +def build_inline_formula_xml(latex: str, run_properties_xml: str) -> str: + """Build one ``a14:m`` inline math zone for insertion inside ``a:p``.""" + try: + omml = compile_latex_to_inline_omml(latex) + except FormulaCompileError as exc: + raise RuntimeError(f"Unsupported inline formula LaTeX: {exc}") from exc + styled = _apply_run_properties(omml, run_properties_xml) + return f"{styled}" + + +def wrap_inline_formula_shape(shape_xml: str) -> str: + """Wrap a text shape containing ``a14:m`` in its required MCE Choice.""" + return ( + f'' + f'' + f"{shape_xml}" + "" + "" + ) + + +__all__ = [ + "INLINE_FORMULA_ATTR", + "build_inline_formula_xml", + "inline_formula_marker_errors", + "wrap_inline_formula_shape", +] diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_attributes.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_attributes.py index 20b6f1f5..7fe942d6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_attributes.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_attributes.py @@ -1,4 +1,4 @@ -"""Canonical and compatible attribute access for chart/table replacements.""" +"""Canonical and compatible attribute access for native replacements.""" from __future__ import annotations @@ -49,7 +49,7 @@ def _resolved_alias( def native_replacement_kind(elem: ET.Element) -> str: - """Return the requested chart/table replacement kind, or an empty string.""" + """Return the requested native replacement kind, or an empty string.""" return _resolved_alias(elem, REPLACE_WITH_ATTR, LEGACY_REPLACE_WITH_ATTR) or "" diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py index f667f70c..db3c90ac 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py @@ -46,7 +46,7 @@ CHARTEX_CONTENT_TYPE = "application/vnd.ms-office.chartex+xml" CHART_COLOR_STYLE_CONTENT_TYPE = "application/vnd.ms-office.chartcolorstyle+xml" CHART_STYLE_CONTENT_TYPE = "application/vnd.ms-office.chartstyle+xml" -_NATIVE_KINDS = {"table", "chart"} +_NATIVE_KINDS = {"table", "chart", "formula"} _POWERPOINT_COORD_MIN = -(2**31) _POWERPOINT_COORD_MAX = 2**31 - 1 _POWERPOINT_LINE_WIDTH_MAX = 20116800 @@ -268,11 +268,11 @@ def native_marker_transform(transform: str | None) -> tuple[float, float, float, operations = parse_transform_operations(raw) except ValueError as exc: raise RuntimeError( - "Native PPTX table/chart markers support translate/scale transforms only" + "Native PPTX replacement markers support translate/scale transforms only" ) from exc if any(name not in {"translate", "scale"} for name, _args in operations): raise RuntimeError( - "Native PPTX table/chart markers support translate/scale transforms only" + "Native PPTX replacement markers support translate/scale transforms only" ) a, b, c, d, e, f = parse_transform_matrix(raw) @@ -281,7 +281,7 @@ def native_marker_transform(transform: str | None) -> tuple[float, float, float, raise RuntimeError("Native PPTX marker transform exceeds finite coordinates") if b != 0.0 or c != 0.0: raise RuntimeError( - "Native PPTX table/chart markers support translate/scale transforms only" + "Native PPTX replacement markers support translate/scale transforms only" ) return e, f, a, d @@ -631,7 +631,7 @@ def _resolved_bounds( ) -> tuple[float, float, float, float, bool]: """Resolve object bounds in SVG px plus whether all bounds were explicit.""" if ctx.use_transform_matrix: - raise RuntimeError("Native PPTX table/chart markers support translate/scale only") + raise RuntimeError("Native PPTX replacement markers support translate/scale only") raw_x = payload.get("x", elem.get("data-pptx-x")) raw_y = payload.get("y", elem.get("data-pptx-y")) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_status.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_status.py index cd59e20e..4a31a5fd 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_status.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_status.py @@ -1,4 +1,4 @@ -"""Validate chart/table replacement fallback and release-route attributes.""" +"""Validate native replacement fallback and release-route attributes.""" from __future__ import annotations @@ -24,7 +24,7 @@ from .marker_attributes import ( VISUAL_STATUSES = frozenset({"source-preview", "normalized", "placeholder"}) ROUTE_STATUSES = frozenset({"reconstruction-only"}) -REPLACEMENT_KINDS = frozenset({"chart", "table"}) +REPLACEMENT_KINDS = frozenset({"chart", "formula", "table"}) # Closed importer outputs from chart_to_svg, chartex_to_svg, and tbl_to_svg. # This includes codes forwarded through their dynamic ``status`` parameters. REPLACEMENT_STATUS_CODES = frozenset({ @@ -126,7 +126,9 @@ def native_marker_status_errors(elem: ET.Element) -> list[str]: and canonical_kind_raw == canonical_kind_raw.strip() and canonical_kind_raw != canonical_kind_raw.lower() ): - errors.append(f"{REPLACE_WITH_ATTR} must use lowercase chart or table") + errors.append( + f"{REPLACE_WITH_ATTR} must use lowercase chart, formula, or table" + ) if visual_raw is not None and visual_raw != visual_raw.strip(): errors.append(f"{FALLBACK_KIND_ATTR} must not contain surrounding whitespace") if legacy_visual_raw is not None and legacy_visual_raw != legacy_visual_raw.strip(): diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py index e448b507..ec8dd186 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py @@ -19,10 +19,10 @@ import zipfile from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import asdict, dataclass from datetime import datetime, timezone -from pathlib import Path, PureWindowsPath +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Any from xml.etree import ElementTree as ET -from xml.sax.saxutils import escape +from xml.sax.saxutils import escape, quoteattr from pptx import Presentation from pptx.util import Emu @@ -55,6 +55,10 @@ from pptx_opc_validation import ( verify_internal_relationships, ) from language_tags import normalize_language_tag +from hyperlink_contract import ( + HYPERLINK_REL_TYPE, + trigger_shape_hyperlink_errors, +) from ..animation_config import ( MorphPair, @@ -144,8 +148,19 @@ PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" P14_NS = "http://schemas.microsoft.com/office/powerpoint/2010/main" +MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006" +A14_NS = "http://schemas.microsoft.com/office/drawing/2010/main" +MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" -for _prefix, _uri in (("p", PML_NS), ("a", DML_NS), ("r", REL_NS), ("p14", P14_NS)): +for _prefix, _uri in ( + ("p", PML_NS), + ("a", DML_NS), + ("r", REL_NS), + ("p14", P14_NS), + ("mc", MC_NS), + ("a14", A14_NS), + ("m", MATH_NS), +): try: ET.register_namespace(_prefix, _uri) except (ValueError, AttributeError): @@ -240,10 +255,15 @@ def _find_relationship_id( rels_path: Path, rel_type: str, target: str, + target_mode: str | None = None, ) -> str | None: """Find an existing relationship by type and target.""" for rel_id, attrs in _read_relationships(rels_path).items(): - if attrs.get("Type") == rel_type and attrs.get("Target") == target: + if ( + attrs.get("Type") == rel_type + and attrs.get("Target") == target + and attrs.get("TargetMode") == target_mode + ): return rel_id return None @@ -423,6 +443,7 @@ _TOP_LEVEL_SHAPE_TAGS = { f"{{{PML_NS}}}pic", f"{{{PML_NS}}}cxnSp", f"{{{PML_NS}}}graphicFrame", + f"{{{MC_NS}}}AlternateContent", } _FLAT_SYSTEM_PLACEHOLDER_TYPES = frozenset({"dt", "ftr", "sldNum"}) _REL_ATTRS = { @@ -641,15 +662,20 @@ def _shape_relationships_supported( elem: ET.Element, rels: dict[str, dict[str, str]], ) -> bool: - """Only image relationships are safe to copy into a slide master here.""" + """Return whether every shape relation can move to Master/Layout parts.""" for rel_id in _relationship_ids_in_shape(elem): attrs = rels.get(rel_id) if not attrs: return False - if attrs.get("TargetMode"): - return False - if attrs.get("Type") != IMAGE_REL_TYPE: - return False + rel_type = attrs.get("Type") + target_mode = attrs.get("TargetMode") + if rel_type == IMAGE_REL_TYPE and not target_mode: + continue + if rel_type == HYPERLINK_REL_TYPE and target_mode == "External": + continue + if rel_type == SLIDE_REL_TYPE and not target_mode: + continue + return False return True @@ -679,7 +705,8 @@ def _canonical_shape_xml( attrs = rels.get(value, {}) node.set( attr_name, - f"{attrs.get('Type', '')}|{attrs.get('Target', '')}", + f"{attrs.get('Type', '')}|{attrs.get('Target', '')}|" + f"{attrs.get('TargetMode', '')}", ) return ET.tostring(clone, encoding="utf-8") @@ -688,11 +715,37 @@ def _ensure_relationship( rels_path: Path, rel_type: str, target: str, + target_mode: str | None = None, ) -> str: - existing = _find_relationship_id(rels_path, rel_type, target) + existing = _find_relationship_id( + rels_path, + rel_type, + target, + target_mode, + ) if existing: return existing - return _append_relationship(rels_path, rel_type, target) + return _append_relationship( + rels_path, + rel_type, + target, + target_mode=target_mode, + ) + + +def _part_name_for_relationships_path(rels_path: Path) -> str: + """Recover one ``ppt/...`` package part from its relationship sidecar.""" + if rels_path.parent.name != "_rels" or not rels_path.name.endswith(".rels"): + raise RuntimeError(f"Invalid PPTX relationship path: {rels_path}") + part_path = rels_path.parent.parent / rels_path.name.removesuffix(".rels") + parts = part_path.parts + try: + ppt_index = len(parts) - 1 - tuple(reversed(parts)).index("ppt") + except ValueError as exc: + raise RuntimeError( + f"Relationship path is not under a ppt package: {rels_path}" + ) from exc + return PurePosixPath(*parts[ppt_index:]).as_posix() def _copy_shape_relationships_to_part( @@ -702,6 +755,7 @@ def _copy_shape_relationships_to_part( ) -> ET.Element: """Clone a shape and retarget supported relationship ids to another part.""" clone = ET.fromstring(ET.tostring(elem, encoding="utf-8")) + target_part = _part_name_for_relationships_path(target_rels_path) for node in clone.iter(): for attr_name, value in list(node.attrib.items()): if attr_name not in _REL_ATTRS: @@ -709,10 +763,22 @@ def _copy_shape_relationships_to_part( rel = slide_rels.get(value) if not rel: raise RuntimeError(f"Missing slide relationship for {value}") + target_mode = rel.get("TargetMode") + relationship_target = rel["Target"] + if target_mode != "External": + resolved_target = _resolve_package_target( + "ppt/slides/source.xml", + relationship_target, + ) + relationship_target = posixpath.relpath( + resolved_target, + posixpath.dirname(target_part), + ) new_rid = _ensure_relationship( target_rels_path, rel["Type"], - rel["Target"], + relationship_target, + target_mode, ) node.set(attr_name, new_rid) return clone @@ -2132,7 +2198,7 @@ def _move_template_static_shape( if not _shape_relationships_supported(shape, state.rels): raise TemplateStructureError( f"{state.spec.svg_path.name}: structure element {item.element_id!r} " - "uses a non-image or external relationship" + "uses a relationship that cannot move to a template part" ) prototype_state = states[0] @@ -3764,6 +3830,8 @@ def _append_relationship( rels_path: Path, rel_type: str, target: str, + *, + target_mode: str | None = None, ) -> str: """Append a relationship entry with the next available rId.""" with open(rels_path, 'r', encoding='utf-8') as f: @@ -3771,9 +3839,14 @@ def _append_relationship( rid_numbers = [int(match) for match in re.findall(r'Id="rId(\d+)"', rels_content)] next_rid = f'rId{max(rid_numbers, default=0) + 1}' + mode_attr = ( + f" TargetMode={quoteattr(target_mode)}" + if target_mode is not None + else "" + ) rel_xml = ( - f' ' + f" " ) rels_content = rels_content.replace( '', rel_xml + '\n', @@ -5329,6 +5402,13 @@ def create_pptx_with_native_svg( explicit_animation_groups = frozenset( explicit_group_ids | trigger_group_ids ) + if trigger_group_ids: + hyperlink_trigger_errors = trigger_shape_hyperlink_errors( + ET.parse(svg_path).getroot(), + trigger_group_ids, + ) + if hyperlink_trigger_errors: + raise ValueError('; '.join(hyperlink_trigger_errors)) converter_group_overrides = ( explicit_animation_groups | frozenset( @@ -5347,7 +5427,9 @@ def create_pptx_with_native_svg( content_type_overrides, ) = ( convert_svg_to_slide_shapes( - svg_path, slide_num=slide_num, verbose=verbose, + svg_path, slide_num=slide_num, + slide_count=public_slide_count, + verbose=verbose, text_flow=text_flow, image_optimize=image_optimize, image_max_dimension=image_max_dimension, @@ -5532,9 +5614,16 @@ def create_pptx_with_native_svg( extra_rels = '' for rel in rel_entries: + target_mode = rel.get('target_mode') + mode_attr = ( + f" TargetMode={quoteattr(target_mode)}" + if target_mode is not None + else '' + ) extra_rels += ( - f'\n ' + f"\n " ) rels_xml = f''' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_validation.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_validation.py index 77e12601..052f6cc4 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_validation.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_validation.py @@ -50,6 +50,7 @@ PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" P14_NS = "http://schemas.microsoft.com/office/powerpoint/2010/main" +MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006" SLIDE_LAYOUT_REL_TYPE = ( "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" ) @@ -79,6 +80,7 @@ _TOP_LEVEL_VISIBLE_TAGS = frozenset({ f"{{{PML_NS}}}graphicFrame", f"{{{PML_NS}}}grpSp", f"{{{PML_NS}}}cxnSp", + f"{{{MC_NS}}}AlternateContent", }) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/applier.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/applier.py index 7285f506..f97718f9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/applier.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_fill_pptx/applier.py @@ -8,10 +8,12 @@ from __future__ import annotations import tempfile import zipfile +from dataclasses import dataclass from pathlib import Path from typing import Any from xml.etree import ElementTree as ET +from hyperlink_contract import SLIDE_JUMP_ACTION from pptx_animations import ( object_animation_fingerprint, validate_pptx_animation_package, @@ -30,7 +32,17 @@ from .chart_fill import ( ) from .clone import _make_part_allocator, deep_clone_slide_private_parts from .notes import _find_notes_master_target, _slide_rels_with_notes -from .ooxml import NS, REL_NS, SLIDE_REL_TYPE, _parse_slide_refs, _qn, _xml_bytes +from .ooxml import ( + NS, + REL_NS, + SLIDE_REL_TYPE, + SlideRef, + _normalize_part, + _parse_slide_refs, + _qn, + _rels_name_for_part, + _xml_bytes, +) from .package import ( _add_notes_override, _add_slide_override, @@ -40,6 +52,7 @@ from .package import ( _max_slide_id, _max_slide_part_number, _prune_unreferenced_parts, + _relative_target, ) from .table_fill import _apply_table_edits_to_slide from .text_fill import _apply_replacements_to_slide @@ -51,6 +64,201 @@ from .transitions import ( ) +@dataclass(frozen=True) +class _PlannedSlideClone: + """One output slide resolved before any source relationship is rewritten.""" + + offset: int + item: dict[str, Any] + source_slide: int + source_ref: SlideRef + slide_number: int + part_name: str + rels_name: str + presentation_rid: str + + +def _build_clone_roster( + plan_slides: list[Any], + slide_refs: dict[int, SlideRef], + *, + next_slide_number: int, + next_rel_number: int, +) -> list[_PlannedSlideClone]: + """Resolve every planned output slide before cloning begins.""" + roster: list[_PlannedSlideClone] = [] + for offset, raw_item in enumerate(plan_slides): + if not isinstance(raw_item, dict): + raise RuntimeError(f"Plan slide {offset + 1} must be an object") + source_slide = int(raw_item.get("source_slide", 0)) + if source_slide not in slide_refs: + raise RuntimeError(f"Plan references a missing source slide: {source_slide}") + slide_number = next_slide_number + offset + roster.append( + _PlannedSlideClone( + offset=offset, + item=raw_item, + source_slide=source_slide, + source_ref=slide_refs[source_slide], + slide_number=slide_number, + part_name=f"ppt/slides/slide{slide_number}.xml", + rels_name=f"ppt/slides/_rels/slide{slide_number}.xml.rels", + presentation_rid=f"rId{next_rel_number + offset}", + ) + ) + return roster + + +_SLIDE_LAYOUT_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" +) +_SLIDE_MASTER_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" +) + + +def _slide_jump_relationship_ids(part_root: ET.Element) -> set[str]: + """Return relationship ids used by actual same-deck click actions.""" + relationship_attr = _qn(NS["r"], "id") + return { + relationship_id + for link in part_root.iter(_qn(NS["a"], "hlinkClick")) + if (link.attrib.get("action") or "").strip() == SLIDE_JUMP_ACTION + if (relationship_id := (link.attrib.get(relationship_attr) or "").strip()) + } + + +def _remap_slide_jump_relationships( + part_root: ET.Element, + relationships_root: ET.Element, + *, + source_owner_part: str, + output_owner_part: str, + owner_label: str, + outputs_by_source_part: dict[str, list[str]], + source_ref_by_part: dict[str, SlideRef], + self_source_part: str | None = None, + self_output_part: str | None = None, +) -> bool: + """Point referenced slide-jump relationships at final output slides. + + Unreferenced ``/slide`` relationships are left untouched. A cloned slide's + self-link follows that clone; shared layout/master links require one unique + output for the referenced source slide. + """ + relationship_ids = _slide_jump_relationship_ids(part_root) + relationships = { + rel.attrib.get("Id", ""): rel + for rel in relationships_root.findall(_qn(REL_NS, "Relationship")) + } + changed = False + for relationship_id in sorted(relationship_ids): + rel = relationships.get(relationship_id) + if rel is None: + raise RuntimeError( + f"{owner_label} has a slide jump with missing relationship " + f"{relationship_id!r}" + ) + if rel.attrib.get("Type") != SLIDE_REL_TYPE: + raise RuntimeError( + f"{owner_label} slide jump {relationship_id!r} does not use " + "a slide relationship" + ) + if rel.attrib.get("TargetMode") == "External": + raise RuntimeError( + f"{owner_label} has an external slide relationship" + ) + target = rel.attrib.get("Target") + if not target: + raise RuntimeError( + f"{owner_label} has a slide relationship without a target" + ) + source_target_part = _normalize_part(target, source_owner_part) + if ( + self_source_part is not None + and self_output_part is not None + and source_target_part == self_source_part + ): + output_target_part = self_output_part + else: + output_targets = outputs_by_source_part.get(source_target_part, []) + source_target_ref = source_ref_by_part.get(source_target_part) + target_label = ( + f"source slide {source_target_ref.index}" + if source_target_ref is not None + else source_target_part + ) + if not output_targets: + raise RuntimeError( + f"{owner_label} links to omitted {target_label}; " + "include the target exactly once or remove the link" + ) + if len(output_targets) > 1: + raise RuntimeError( + f"{owner_label} links to repeated {target_label}; " + "the output target is ambiguous" + ) + output_target_part = output_targets[0] + output_target = _relative_target(output_owner_part, output_target_part) + if rel.attrib.get("Target") != output_target: + rel.set("Target", output_target) + changed = True + return changed + + +def _remap_reachable_shared_layer_slide_jumps( + entries: dict[str, bytes], + clone_roster: list[_PlannedSlideClone], + *, + outputs_by_source_part: dict[str, list[str]], + source_ref_by_part: dict[str, SlideRef], +) -> None: + """Remap links inherited from layouts and masters used by output slides.""" + pending: list[str] = [] + for clone in clone_roster: + rels_data = entries.get(clone.rels_name) + if not rels_data: + continue + rels_root = ET.fromstring(rels_data) + for rel in rels_root.findall(_qn(REL_NS, "Relationship")): + if rel.attrib.get("Type") != _SLIDE_LAYOUT_REL_TYPE: + continue + target = rel.attrib.get("Target") + if target: + pending.append(_normalize_part(target, clone.part_name)) + + visited: set[str] = set() + while pending: + part_name = pending.pop() + if part_name in visited: + continue + visited.add(part_name) + part_data = entries.get(part_name) + rels_name = _rels_name_for_part(part_name) + rels_data = entries.get(rels_name) + if not part_data or not rels_data: + continue + part_root = ET.fromstring(part_data) + rels_root = ET.fromstring(rels_data) + changed = _remap_slide_jump_relationships( + part_root, + rels_root, + source_owner_part=part_name, + output_owner_part=part_name, + owner_label=part_name, + outputs_by_source_part=outputs_by_source_part, + source_ref_by_part=source_ref_by_part, + ) + if changed: + entries[rels_name] = _xml_bytes(rels_root) + for rel in rels_root.findall(_qn(REL_NS, "Relationship")): + if rel.attrib.get("Type") != _SLIDE_MASTER_REL_TYPE: + continue + target = rel.attrib.get("Target") + if target: + pending.append(_normalize_part(target, part_name)) + + def apply_plan( pptx_path: Path, plan: dict[str, Any], @@ -90,15 +298,25 @@ def apply_plan( allocate_part = _make_part_allocator(entries) wrote_auto_advance = False - for offset, item in enumerate(plan_slides): - source_slide = int(item.get("source_slide", 0)) - if source_slide not in slide_refs: - raise RuntimeError(f"Plan references a missing source slide: {source_slide}") - source_ref = slide_refs[source_slide] - new_slide_number = next_slide_number + offset - new_part = f"ppt/slides/slide{new_slide_number}.xml" - new_rels = f"ppt/slides/_rels/slide{new_slide_number}.xml.rels" - new_rid = f"rId{next_rel_number + offset}" + clone_roster = _build_clone_roster( + plan_slides, + slide_refs, + next_slide_number=next_slide_number, + next_rel_number=next_rel_number, + ) + source_ref_by_part = {ref.part_name: ref for ref in slide_refs.values()} + outputs_by_source_part: dict[str, list[str]] = {} + for clone in clone_roster: + outputs_by_source_part.setdefault(clone.source_ref.part_name, []).append(clone.part_name) + + for clone in clone_roster: + item = clone.item + source_slide = clone.source_slide + source_ref = clone.source_ref + new_slide_number = clone.slide_number + new_part = clone.part_name + new_rels = clone.rels_name + new_rid = clone.presentation_rid source_slide_xml = entries[source_ref.part_name] source_animation_fingerprint = object_animation_fingerprint( @@ -143,6 +361,17 @@ def apply_plan( source_rels = entries.get(source_ref.rels_name) slide_rels_root = ET.fromstring(source_rels) if source_rels else _empty_relationships_root() + _remap_slide_jump_relationships( + slide_root, + slide_rels_root, + source_owner_part=source_ref.part_name, + output_owner_part=new_part, + owner_label=f"Source slide {source_slide}", + outputs_by_source_part=outputs_by_source_part, + source_ref_by_part=source_ref_by_part, + self_source_part=source_ref.part_name, + self_output_part=new_part, + ) deep_clone_slide_private_parts( slide_rels_root, new_slide_part=new_part, @@ -203,9 +432,19 @@ def apply_plan( ET.SubElement( sld_id_lst, _qn(NS["p"], "sldId"), - {"id": str(next_slide_id + offset), _qn(NS["r"], "id"): new_rid}, + { + "id": str(next_slide_id + clone.offset), + _qn(NS["r"], "id"): new_rid, + }, ) + _remap_reachable_shared_layer_slide_jumps( + entries, + clone_roster, + outputs_by_source_part=outputs_by_source_part, + source_ref_by_part=source_ref_by_part, + ) + entries["ppt/presentation.xml"] = _xml_bytes(pres_root) entries["ppt/_rels/presentation.xml.rels"] = _xml_bytes(pres_rels_root) _prune_unreferenced_parts(entries, content_root) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md index d657d3dc..0d9f3aca 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md @@ -43,7 +43,6 @@ Start with this exact heading order: | Reading Mode | | | Content Strategy | | | Design Style | | -| Formula Policy | | | AI Image Acquisition Path | | | Generation Mode | | | Spec Refinement | | @@ -179,7 +178,7 @@ in `kebab-case` and add one `Native-ready` map: `=yes|no; ...`. Use `yes` only when editable native output helps. Qualitative relationships/read order remain unkeyed prose, as do incidental microvisuals. -In §VIII, author every planned or explicitly required resource from the confirmed source boundary. Write one concise, non-empty `Layout pattern` suggestion in ordinary language; optionally cite hierarchical ids from the layout library when they help recall a technique. Set `Crop Policy` to `adaptive` or `no-crop`; set `Acquire Via` to `ai`, `web`, `user`, `formula`, `placeholder`, or `slice`. Preserve unresolved required assets as `Pending` or `Needs-Manual` instead of dropping or reclassifying them. +In §VIII, author every planned or explicitly required resource from the confirmed source boundary. Write one concise, non-empty `Layout pattern` suggestion in ordinary language; optionally cite hierarchical ids from the layout library when they help recall a technique. Set `Crop Policy` to `adaptive` or `no-crop`; set `Acquire Via` to `ai`, `web`, `user`, `placeholder`, or `slice`. Preserve unresolved required assets as `Pending` or `Needs-Manual` instead of dropping or reclassifying them. Native formulas never enter this table or `spec_lock.md images`. §VIII `Layout pattern` is a per-resource preference. When a page uses several images, repeats one image in multiple views, or combines an image with native overlays, describe the page-level relationship and participating resources in §IX `Layout` / `Images`; do not duplicate an unchanged resource row merely to encode animation sequencing. @@ -201,6 +200,7 @@ Write one ordered Slide block per page. Slide count and order must equal §I `Pa - **Title**: - **Core message**: - **Content**: +- **Mathematical content**: ## X. Speaker Notes Requirements @@ -228,7 +228,7 @@ never write an empty or `none` placeholder: - **Motion suggestion**: ``` -Add `Visualization` / `Images` when a Slide consumes §VII/§VIII or uses a page-local visual model. Name every value-driven geometry, qualitative relationship, cell grid, and child visual here; only independent Chart/Table entries use object keys. Describe qualitative order, linkage, hierarchy, grouping, contrast, overlap, and reading path freely—not as a model name or grammar enum. §IX may choose a custom Chart/Table fallback. Add `Native shape suggestion` only when a preset, stock Connector, or compound silhouette/cutout/intersection/fragment may help; name the semantic result plus candidate family or Boolean operands, never implementation geometry or keys. Executor chooses the primitive, preset, Boolean construction, or necessary freeform. Add `Motion suggestion` whenever transition/reveal advice strengthens communication, regardless of the Custom Animations outcome; state purpose and semantic order/relationship, not registry keys, options, timing, ids, or coverage. The suggestion never activates animation execution by itself, creates content, or binds implementation. Describe required visible image states in `Layout` / `Images` only for an explicit motion requirement or an enabled Custom Animations outcome. Add keyed `Native-ready` only for independent data charts or pure text-grid tables, `Fact IDs` for sourced claims, and `Data class: scenario` for invented demo values. Except on preservation paths, `Cover impact` carries a binding hook and adaptable composition; apply the same split to `Closing impact` only when the deck genuinely resolves. Roster/order/content stay authoritative. §VIII image layout is non-empty free prose with optional library ids; §VII Chart/Table rows are references. Executor owns geometry, hierarchy, treatment, and sparse local garnish. +Add `Mathematical content` whenever a Slide needs a mathematical expression preserved exactly. Store the expression body as valid LaTeX without `$...$`, `$$...$$`, `\(...\)`, or `\[...\]` source delimiters; the field does not classify inline versus structural use. This is content authority for [`native-formula.md`](../references/native-formula.md), not a formula policy, marker, or implementation request; Executor chooses ordinary text, inline native math, or block native math. Add `Visualization` / `Images` when a Slide consumes §VII/§VIII or uses a page-local visual model. Name every value-driven geometry, qualitative relationship, cell grid, and child visual here; only independent Chart/Table entries use object keys. Describe qualitative order, linkage, hierarchy, grouping, contrast, overlap, and reading path freely—not as a model name or grammar enum. §IX may choose a custom Chart/Table fallback. Add `Native shape suggestion` only when a preset, stock Connector, or compound silhouette/cutout/intersection/fragment may help; name the semantic result plus candidate family or Boolean operands, never implementation geometry or keys. Executor chooses the primitive, preset, Boolean construction, or necessary freeform. Add `Motion suggestion` whenever transition/reveal advice strengthens communication, regardless of the Custom Animations outcome; state purpose and semantic order/relationship, not registry keys, options, timing, ids, or coverage. The suggestion never activates animation execution by itself, creates content, or binds implementation. Describe required visible image states in `Layout` / `Images` only for an explicit motion requirement or an enabled Custom Animations outcome. Add keyed `Native-ready` only for independent data charts or pure text-grid tables, `Fact IDs` for sourced claims, and `Data class: scenario` for invented demo values. Except on preservation paths, `Cover impact` carries a binding hook and adaptable composition; apply the same split to `Closing impact` only when the deck genuinely resolves. Roster/order/content stay authoritative. §VIII image layout is non-empty free prose with optional library ids; §VII Chart/Table rows are references. Executor owns geometry, hierarchy, treatment, and sparse local garnish. For free-design pages, describe `Layout` through relationships, hierarchy, regions, and column spans; do not prescribe element-level `x`, `y`, `width`, or `height` or duplicate the global geometry in §II/§V. Exact coordinates belong to Executor SVG authoring. Preserve literal geometry only when the user explicitly requires it or a mirror/template preservation contract owns it. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/scaffolds/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/scaffolds/design_spec.md index 59da2c97..9c654d61 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/scaffolds/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/scaffolds/design_spec.md @@ -17,7 +17,6 @@ | Reading Mode | [fill] | | Content Strategy | [fill] | | Design Style | [fill] | -| Formula Policy | [fill] | | AI Image Acquisition Path | [fill or not applicable] | | Generation Mode | [fill] | | Spec Refinement | [fill] | @@ -115,6 +114,7 @@ - **Title**: [fill] - **Core message**: [fill] - **Content**: [fill] +- **Mathematical content**: [fill exact delimiter-free LaTeX expression body, or omit] ## X. Speaker Notes Requirements diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/generate-pptx.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/generate-pptx.md index 2d7e7f14..5c1e19db 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/generate-pptx.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/generate-pptx.md @@ -25,9 +25,9 @@ request does not explicitly select Quick. | Scope | Contract | |---|---| -| Any route that authors or regenerates slide visuals through SVG | `svg_output/` is the complete page-design source: every visible text, image, shape, chart/table fallback, and layout element that should appear on the exported slide is present in that page SVG or referenced by it. | +| Any route that authors or regenerates slide visuals through SVG | `svg_output/` is the complete page-design source: every visible text, image, shape, chart/table fallback, block/inline native-formula preview, and layout element that should appear on the exported slide is present in that page SVG or referenced by it. | | Templates, `design_spec.md`, and `spec_lock.md` | Authoring/control inputs. They guide SVG creation but MUST NOT supply visible slide content that is absent from the completed SVG during export. | -| Semantic SVG markers | Minimal rendering-neutral compiler hints used only after existing Layout/Layer/Placeholder/Native metadata has been considered. They never replace native SVG geometry, text, styles, grouping, or asset references. | +| Semantic SVG markers | Minimal rendering-neutral compiler hints used only after existing Layout/Layer/Placeholder/Native metadata has been considered. Chart/table markers preserve their visible SVG fallback; block and inline formula markers carry exact LaTeX and replace only their registered ordinary SVG preview with editable Office Math during PPTX export. | | `svg_final/` | Mandatory derived, self-contained SVG visual preview in the default pipeline. It may be opened directly or inserted into PowerPoint as an SVG picture, but it is not a supported PPTX source and carries no manual Convert-to-Shape compatibility contract. Quick-generate skips it. | | SVG-to-PPTX export | The only supported generated-PPTX route reads `svg_output/` and maps its content through the project converter to DrawingML/native objects. It compiles only the selected route's explicit structure contract: `flat` keeps represented content Slide-local, while `structured` may place explicitly scoped content in Master/Layout/Slide parts. It MUST NOT infer structure, upgrade `flat`, or invent new visible page content. | | Native PPTX routes and presentation-behavior stages | Remain outside SVG page-design closure. `template-fill-pptx`, `native-enhance-pptx`, animations, transitions, speaker notes, narration, and package relationships are not required to round-trip through SVG. | @@ -255,16 +255,16 @@ Then load only the extra role modules triggered by the current plan: |---|---| | Stage 1 is confirmed and its template choice installed a selected Brand/Style/Layout/Deck workspace into this project | `references/strategist-template.md` before Stage 2 | | The confirmed Stage-1 `delivery_context` identifies recorded/self-running/video delivery, or input is an explicit final/literal narration script | `references/video-design.md` before the three Stage-2 whole solutions and page roster | -| The confirmed Stage 2 `image_usage` contains a source other than `none`, the user supplied an explicit non-`none` image constraint, or formula-worthy content activates formula planning | `references/image-layout-spec.md` + `references/image-layout-patterns.md` before production detail, formula resources, or §VIII | +| The confirmed Stage 2 `image_usage` contains a source other than `none`, or the user supplied an explicit non-`none` image constraint | `references/image-layout-spec.md` + `references/image-layout-patterns.md` before production detail or §VIII | After Stage 1 and template handoff, load `strategist-image.md` plus only the three `_index.md` files. Author the three whole solution intents before mapping any component basis. Freeze every referenced mode/style/rendering id from the indexes, then read once only the deduplicated union of those exact detail files and finish the three custom behaviors. A novel custom reads no detail file. -Confirmed non-`none` or formulas load the layout references and continue into -resource planning; confirmed `none` without formulas writes no image rows while -retaining recommendation-only rendering candidates. Only an installed +Confirmed non-`none` loads the layout references and continues into resource +planning; confirmed `none` writes no image rows while retaining +recommendation-only rendering candidates. Only an installed project-local template state loads the template module, and only after Stage 1 is confirmed; a bare template/style name does not. @@ -312,7 +312,7 @@ Stage-2 summary without fabricating UI results. Silence confirms nothing. | `confirm_ui/template_options.json` | Candidate schema/language plus supplied exact roots; library entries remain server-owned index data | Stage-1 submission writes user-owned `template_selection.json` with `phase: template`, `status: confirmed` | | `confirm_ui/recommendations.stage1.json` | Communication contract, `content_divergence`, and canvas only; no template-derived recommendation | The same submission writes `result.json` with `status: stage1-confirmed` | | `confirm_ui/template_handoff.json` | Only through `--complete-template-selection`, after the Stage-1 selection and free-design closure or successful installation | `status: ready`, bound to the current selection hash; prerequisite for Stage 2 | -| `confirm_ui/recommendations.stage2.json` | `stage: stage2`; complete deck solution plus conditional AI path, formula policy, generation mode, refine-spec, proactive speaker notes, custom animations, and narration audio | `stage: final`, `status: confirmed` | +| `confirm_ui/recommendations.stage2.json` | `stage: stage2`; complete deck solution plus conditional AI path, generation mode, refine-spec, proactive speaker notes, custom animations, and narration audio | `stage: final`, `status: confirmed` | If the user rejects the current recommendation before confirming it, regenerate by overwriting that same stage file and have the page refresh; do not create revision-suffixed files. This never authorizes one stage file to carry another stage's payload. @@ -415,7 +415,23 @@ For the normal/default `continuous` path, print no split-mode reminder and proce **Mandatory — spec-refinement note** (not another Confirm UI stage): after confirmation details and any split-mode line, append one localized 💡 line offering review of the complete Design Spec before the lock; any part may be revised in chat until explicit approval. Default OFF; only explicit chat opt-in or `refine_spec: true` runs [`refine-spec`](stages/refine-spec.md) after Gate 1. Confirm UI records the toggle; chat fallback prints the same line. -**Formula policy**: Final Stage 2 confirms `mixed`, `render-all`, or `text-only`. The image-decision core is already loaded; when rendering is required, load the conditional image-layout references even if `image_usage` is `none`, then follow [`strategist-image.md`](../references/strategist-image.md)'s formula-resource contract. `text-only` creates no formula image rows. +**Native formula content**: Formula handling is not a confirmation field or an +image-acquisition path. Strategist records exact mathematical content as a +delimiter-free LaTeX expression body in the applicable §IX page block without +classifying its implementation. Executor independently chooses ordinary text, +same-paragraph native inline math, or a standalone native block under +[`native-formula.md`](../references/native-formula.md); matrices, multiline +derivations, and other high-structure expressions remain blocks. +No formula manifest, §VIII resource row, or `spec_lock.md images` entry is +created. + +**Native hyperlink content**: Hyperlinks are not a confirmation field or a +resource-acquisition path. Strategist records the linked text/object intent and +exact absolute URI or 1-based same-deck slide target in the applicable §IX page +block. Executor chooses an inline or whole-object carrier and authors the +canonical SVG `` under +[`native-hyperlinks.md`](../references/native-hyperlinks.md). Unknown targets +return upstream; no hyperlink manifest or `spec_lock.md` entry is created. **Proactive production decisions**: Final Stage 2 records `proactive_speaker_notes`, `proactive_custom_animations`, and @@ -437,7 +453,7 @@ neither row and ask one question: disable audio too, or retain its required notes. Wait, then update both. Before `generate-audio`, create and split notes when complete per-slide files are absent. -If the user provided images or formula PNGs were rendered, run analysis **before outputting the design spec**. It writes `analysis/image_analysis.csv` — the authoritative regenerated image-fact view in the `analysis/` folder, which MUST be read before authoring §VIII: +If the user provided images, run analysis **before outputting the design spec**. It writes `analysis/image_analysis.csv` — the authoritative regenerated image-fact view in the `analysis/` folder, which MUST be read before authoring §VIII: ```bash python3 ${SKILL_DIR}/scripts/analyze_images.py /images ``` @@ -468,15 +484,15 @@ state and §X records its source/verbatim policy. After Gate 2, before Step 5 or split handoff, write the exact segments once to `notes/total.md`; split them only in Step 7.1. This is frozen production input, not a third planning artifact. -**✅ Internal checkpoint — Phase deliverables complete**: facts read; confirmation consumed once; final Stage-2 production fields resolved (formula policy, generation mode, refine-spec, proactive choices, and conditional AI path); Design Spec passed Gate 1; enabled refinement approved; lock derived from it; split handling resolved; communication and every §IX `Audience move` validated. Do not print this checklist; auto-proceed. +**✅ Internal checkpoint — Phase deliverables complete**: facts read; confirmation consumed once; final Stage-2 production fields resolved (generation mode, refine-spec, proactive choices, and conditional AI path); mathematical content recorded where applicable; Design Spec passed Gate 1; enabled refinement approved; lock derived from it; split handling resolved; communication and every §IX `Audience move` validated. Do not print this checklist; auto-proceed. --- ### Step 5: Image Acquisition Phase (Conditional) -🚧 **GATE**: Step 4 complete; `/design_spec.md` and `/spec_lock.md` both exist. If either required artifact is missing, stop before any acquisition or generation and follow [`failure-recovery.md`](governance/failure-recovery.md) §3. Formula rows already have `Acquire Via: formula` and status `Rendered` or `Needs-Manual`. +🚧 **GATE**: Step 4 complete; `/design_spec.md` and `/spec_lock.md` both exist. If either required artifact is missing, stop before any acquisition or generation and follow [`failure-recovery.md`](governance/failure-recovery.md) §3. -> **Trigger**: At least one row in the resource list has `Acquire Via: ai`, `web`, and/or `slice`, or any row is a pending prepared derivative declared by `Reference: Derived from ; treatment=...`. A prepared-user-only plan skips this step only when it has no derivative to materialize; `formula` and `placeholder` rows alone do not trigger it. A permitted but unused image source creates no row and does not trigger acquisition. If §VIII omits a source, asset, or page role that `image_notes` explicitly requires, the Design Spec is incomplete; return to Step 4 Gate 1, repair it from the retained final state, and re-author the affected lock anchors from context. Do not reopen `result.json` during this check. +> **Trigger**: At least one row in the resource list has `Acquire Via: ai`, `web`, and/or `slice`, or any row is a pending prepared derivative declared by `Reference: Derived from ; treatment=...`. A prepared-user-only plan skips this step only when it has no derivative to materialize; `placeholder` rows alone do not trigger it. A permitted but unused image source creates no row and does not trigger acquisition. If §VIII omits a source, asset, or page role that `image_notes` explicitly requires, the Design Spec is incomplete; return to Step 4 Gate 1, repair it from the retained final state, and re-author the affected lock anchors from context. Do not reopen `result.json` during this check. **Failure recovery**: stop/continue behavior for AI/web/slice/image-readiness failures is defined in [`workflows/governance/failure-recovery.md`](governance/failure-recovery.md). This Step keeps the acquisition procedure. @@ -494,7 +510,7 @@ Then **lazy-load the path-specific reference** for each row that actually needs | `ai` | `references/image-generator.md` | write `/images/image_prompts.json`, then follow `image-generator.md §7 Path Selection` (`image_gen.py --manifest` is **Path A only**) | | `web` | `references/image-searcher.md` | `python3 ${SKILL_DIR}/scripts/image_search.py ...` (≥2 web rows → `--batch images/image_queries.json`) | | `slice` | `references/image-generator.md` §4.3 | derived — **after** the parent `ai` sheet row is `Generated`, run `python3 ${SKILL_DIR}/scripts/slice_images.py /images/.png --grid RxC --names ... --trim --alpha` (see workflow step 2.5) | -| `user` / `formula` / `placeholder` | (skip) | (skip) | +| `user` / `placeholder` | (skip) | (skip) | A deck with only `ai` rows never loads `image-searcher.md`; a deck with only `web` rows never loads `image-generator.md`. A mixed deck loads both, processes each row through its own path, and writes both `image_prompts.json` and `image_sources.json`. @@ -576,7 +592,9 @@ Keep the core's shared visual-quality defaults and `svg-effects.md` §6.1 Visual | Mandatory per-page Structure decision from §IX is `yes` | `executor-structure.md` before any geometry for the first applicable page | | Actual row × column fact grid | `executor-table.md` | | Used preset pattern fill, or independent Chart/Table with §IX `=yes` | `native-data-interface.md` before that object | -| `spec_lock.md images` / §VIII has an image/formula row, or the template has bundled images | `executor-image.md` + `image-layout-spec.md` + `image-layout-patterns.md` + `svg-image-embedding.md` | +| §IX or current page content contains mathematical notation that may require native math | `native-formula.md` before choosing ordinary text, inline native math, or block native math | +| §IX or current page content requires an external or same-deck click hyperlink | `native-hyperlinks.md` before authoring its inline or whole-object SVG anchor | +| `spec_lock.md images` / §VIII has an image row, or the template has bundled images | `executor-image.md` + `image-layout-spec.md` + `image-layout-patterns.md` + `svg-image-embedding.md` | | At least one placed image is `Status: Sourced` or its filename has an `image_sources.json` record | `executor-web-image.md` after the image branch | | §I records recorded/self-running/video delivery, or §X records a final/literal narration script | `video-design.md` before the first SVG; retain it through notes/motion handling | | All SVG pages and SVG quality gates are complete, and the effective Speaker Notes outcome in `design_spec.md §I` is enabled | `executor-notes.md` before generating speaker notes | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/governance/failure-recovery.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/governance/failure-recovery.md index 87bff556..114e1bd3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/governance/failure-recovery.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/governance/failure-recovery.md @@ -24,7 +24,7 @@ Global stop/continue rules for all four top-level routes, plus concrete failure | Execution exposes a missing Strategist-owned role/plan detail | Yes for the affected page | Repair affected Design Spec/lock fragments under [`executor-base.md`](../../references/executor-base.md) §2.1 | Only if confirmed intent changes | Step 4 Gate 1/2 → Step 6 current page | | Execution context is fresh, resumed, restarted, compacted/summary-only, external, or unknown | Yes until rebuilt | Read complete Design Spec, then lock, once; reload triggered inputs and latest completed SVG when mid-deck | No | Step 6 current page | | Step 3 rejects a legacy or incomplete template contract | Yes | Stop template consumption; create a new current workspace through Create Template from the original PPTX/reference, then return with its exact workspace root | Only when required source evidence or template choices are unavailable | Create Template → Generate PPTX Step 3 | -| Formula rendering provider failure | No until the Step 7 readiness gate | Exhaust the provider chain; if unresolved, mark only the affected formula rows `Needs-Manual` and continue | Supply the exact target PNG or change formula policy | Step 4 / Step 7 image readiness gate | +| Native formula marker validation or LaTeX compilation failure | Yes for the affected page | Repair the marker metadata or source LaTeX and rerun the SVG checker; there is no formula-image fallback | Clarify the intended equation only when the source is ambiguous | Active SVG authoring step | | AI image generation failure | No | `auto`: follow A → B → Offline Manual. Explicit `api` / `host-native`: retry only that path, then mark the row `Needs-Manual` without switching automated providers | Only when missing files are required before export | Step 5 / Step 7 image readiness gate | | Web image search/download failure | No | Adjust query/source per image-searcher rules, then mark `Needs-Manual` if unresolved | Only if the resource is required and no acceptable substitute exists | Step 5 | | Slice sheet missing | Yes for derived slice rows | Wait for parent sheet; run `slice_images.py`; rerun image analysis | Yes when sheet was manual/offline | Step 5 slice handling / Step 7 image readiness gate | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/native-enhance-pptx.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/native-enhance-pptx.md index 7d8b0695..b34ce9ed 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/native-enhance-pptx.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/native-enhance-pptx.md @@ -16,6 +16,7 @@ This route treats a `.pptx` as the artifact to preserve. It archives the source |---|---| | Source file | If already under `projects/`, move it into the enhancement project; otherwise copy it | | Visible slides | Do not rewrite existing text, shapes, images, charts, tables, masters, or layouts | +| Existing hyperlinks | Preserve hyperlink XML and relationships unchanged | | Route | Direct PPTX package patching; no SVG conversion | | Output | A new `.pptx` under `/exports/` | | Project kind | `native_pptx_enhancement` | @@ -53,7 +54,7 @@ source.pptx | `narration.timings` | Enabled | Set narrated slides to auto-advance from page-start lead-in, audio duration, and page-tail padding | | `narration.transitions` | Enabled | Add page-level transitions for narrated/selected slides | | `delivery.check` | Enabled | Read-only package/font/media/hidden-slide/file-size and existing-motion audit | -| `media` | Planned | Background music, video, media compression | +| `delivery.compress` | Planned | Reduce embedded media size for oversized packages; the audit above already reports them | | `presenter` | Planned | Q&A notes, speaker cues, rehearsal artifacts | | `animation` | Planned | Explicit object-level animation only | | `visible-stamp` | Planned | Watermark/footer/logo; requires explicit confirmation | @@ -360,7 +361,9 @@ Patch scope: | `ppt/presProps.xml` | `showPr useTimings=1` only when this run writes automatic slide advance | | `[Content_Types].xml` | Required content types | -**Hard rule**: Do not modify existing slide shapes, text bodies, images, chart data, master/layout parts, or existing non-target relationships. +**Hard rule**: Do not modify existing slide shapes, text bodies, images, chart +data, master/layout parts, hyperlink XML/relationships, or existing non-target +relationships. Before publishing the candidate, apply validates transitions, timing/object animation structure, ZIP integrity, unique parts, internal relationships, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/profiles/quick-generate.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/profiles/quick-generate.md index 77bc6d27..e87da1b3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/profiles/quick-generate.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/profiles/quick-generate.md @@ -24,7 +24,7 @@ Page count alone never activates or blocks this profile. | Execution memory | Keep routine page, visual, and resource decisions only in the current active context; losing that context restarts Quick instead of reconstructing a plan from project files | | Inputs | Any supported Generate input; convert/import sources and run bounded factual research when the input requires them | | Templates | Directly validate and install at most one exact workspace root per kind supplied for this run; when none are supplied, use free design without catalog selection or Confirm UI | -| Resources | Prepare every project-local image, icon, formula, and required provenance/manifest artifact before its SVG; sound waits for §4 | +| Resources | Prepare every project-local image, icon, and required provenance/manifest artifact before its SVG; author native formula markers and hyperlink anchors directly in the affected SVG; sound waits for §4 | | Planning artifacts | Do not author a root project `design_spec.md`, `spec_lock.md`, confirmation payloads, or any substitute planning artifact; installed `templates/design_spec...md` files remain template input | | Traceability | Operational resource manifests, checker reports, postflight, and bounded Python command/outcome audit entries may remain, but they do not record the AI's design reasoning or form a resumable generation history | | Delivery | Hand-author the resolved SVG roster, run one lockless final checker, skip `finalize_svg.py`, and export the final native PPTX through `--quick-generate` | @@ -274,9 +274,11 @@ the roster after the whole-roster check: communication job, a recognizable invariant, and deliberate variation across applicable page roles; omit it when restraint serves the deck better; - the resource decisions needed for immediate preparation. Required operational - image/formula manifests may carry filenames, page relationship, status, and + image manifests may carry filenames, page relationship, status, and generation/crop/focal cues, but do not create a general resource roster or an - icon-to-page assignment; + icon-to-page assignment. Keep each selected formula's source LaTeX in active + context for direct marker authoring; retain each selected hyperlink's exact + absolute URI or 1-based same-deck target; create no formula/link manifest; - the implementation path for each resource. An explicit user path wins; otherwise choose the registered automatic/default path without another interaction. @@ -326,7 +328,7 @@ because Quick is expected to be faster is not. | Values, categories, time, weights, or duration determine mark geometry | Value-driven chart | | Sequence, hierarchy, role, region, or relationship determines page-local topology | Qualitative structure | | Rows, columns, cells, headers, merges, and alignment form the information model | Cell-grid table | -| Mathematical notation is clearer as typeset math than ordinary text | Rendered formula asset | +| Mathematical notation is clearer as typeset math than ordinary text | PowerPoint-native inline or block math | | Typography, spacing, and simple geometry already carry the message | Use no additional visual carrier | This carrier menu does not satisfy or replace the per-page Structure decision in §3. @@ -367,7 +369,7 @@ Prepare only the resource paths needed by the decided pages: | Supplied/extracted image | Copy the selected file into `images/`; preserve its factual/provenance context and use the measured file rather than an invented substitute | | Image-to-PPTX reconstruction asset | In Codex, preserve identity graphics through an exact vector, deterministic redraw, sufficient source asset, or reference-based high-resolution reconstruction; keep data graphics native-and-verified or exact. For scene imagery, build the minimum registered clean-base/midground/subject/foreground group; batch padded-bbox-disjoint objects into one shared plate, then split them with grid slicing or independent nested-SVG bbox crops | | Bundled/custom icon | Follow the [icon library contract](../../templates/icons/README.md), choose one coherent primary library, sync a useful project pool covering recurring semantics and likely page-local needs without assigning icons to pages, and choose from that prepared pool during SVG authoring | -| Formula | Follow the [`latex_render.py` contract](../../scripts/docs/image.md), write `images/formula_manifest.json`, run the renderer, and keep the rendered PNG under `images/` | +| Formula | Create no resource file. Retain the exact source LaTeX, then choose ordinary text, an inline native marker, or a block native marker under §3; the registered SVG preview is discarded by native export | | AI image | Follow `image-base.md` + `image-generator.md`; apply only the chosen rendering preset or exact custom bases, never blend unselected catalog identities, and keep `image_prompts.json` plus its human-readable sidecar | | Web image | Follow `image-base.md` + `image-searcher.md`; keep query/status data and `image_sources.json`, including any required on-slide attribution | | Illustration slice | Generate or obtain the parent sheet, run `slice_images.py`, and place only the resulting element files | @@ -389,11 +391,12 @@ After image resources change, run `analyze_images.py` so Operational manifests and provenance are resource truth, not a hidden design strategy. -Every required resource must reach a usable terminal state before its page. -`Needs-Manual` blocks Quick even when an unverified file exists. After manual -supply/replacement, validate evidence and reconcile to `Existing`, `Generated`, -`Sourced`, or `Rendered`; never bypass status by file presence or substitute -unrelated material. +Every required file-backed resource must reach a usable terminal state before +its page. `Needs-Manual` blocks Quick even when an unverified file exists. After +manual supply/replacement, validate evidence and reconcile to `Existing`, +`Generated`, or `Sourced`; never bypass status by file presence or substitute +unrelated material. Native formula markers are authored page content, not +file-backed resources or terminal-status rows. --- @@ -414,7 +417,7 @@ without reading or inventing a nearby preset. Do not load `executor-base.md`: it owns Default's persisted-plan handoff, first-page gate, and completion routing. Excluding that file is not a capability exclusion; Quick loads the shared and conditional execution authorities here -directly. When any image/formula exists, read once before the first affected +directly. When any image exists, read once before the first affected page and reuse throughout the valid execution context: [`executor-image.md`](../../references/executor-image.md), [`image-layout-spec.md`](../../references/image-layout-spec.md), @@ -429,16 +432,25 @@ omit shape-composition reasoning. Reuse it throughout the valid execution context; reread only after a known file change or context invalidation. **Mandatory — per-image-page composition decision**: For every page with one -or more non-formula images, after its content and communication move are +or more images, after its content and communication move are determined but before choosing geometry, apply [`executor-image.md`](../../references/executor-image.md)'s active image-integration decision once. Keep its role, direction source, parent contour, slot/rhythm system, image/shape action, and any continuity only in active context; create no artifact, spec, lock, manifest, or extra pass. A deliberate plain or equal-grid result remains valid when it communicates the -relationship better. Formula-only pages use -[`image-layout-spec.md`](../../references/image-layout-spec.md) without forcing a -multi-image system. +relationship better. + +**Mandatory — native formulas**: Quick creates no formula resource or manifest; +retain exact LaTeX in active context, then choose ordinary text, same-paragraph +native inline math, or a standalone native block and author its matching SVG +preview under [`native-formula.md`](../../references/native-formula.md). + +**Mandatory — native hyperlinks**: Quick creates no hyperlink resource or +manifest. For every selected link, retain the exact target, choose an inline or +whole-object carrier, and author canonical SVG `` under +[`native-hyperlinks.md`](../../references/native-hyperlinks.md). Never guess an +unknown destination. Image to PPTX replaces this open composition decision for its canonical page frame: preserve the source geometry, restore text natively, preserve @@ -465,6 +477,8 @@ capability menu, visualization recall, template geometry, or a later check. | A selected primary Chart/Table `family/key` | [`executor-visualization.md`](../../references/executor-visualization.md), then the matching Chart/Table authority | | Any actual value-driven geometry, including mini/inset charts and sparklines | [`executor-chart.md`](../../references/executor-chart.md) | | Any actual row × column fact grid | [`executor-table.md`](../../references/executor-table.md) | +| Any mathematical notation that may require native math | [`native-formula.md`](../../references/native-formula.md) before choosing ordinary text, inline native math, or block native math | +| Any external or same-deck click hyperlink | [`native-hyperlinks.md`](../../references/native-hyperlinks.md) before authoring its inline or whole-object SVG anchor | | A used preset pattern fill, or one independent Chart/Table object selected as native-ready in active context | [`native-data-interface.md`](../../references/native-data-interface.md) before drawing that object | | Any data-driven chart geometry | [`verify-charts.md`](../stages/verify-charts.md) after the complete roster and before the one final checker | @@ -612,7 +626,9 @@ or lock. - [x] All required source/resource preparation is complete - [x] One mode and visual style were resolved, and every catalog source actually used was read - [x] Every page considered the complete visual-carrier menu without a coverage quota -- [x] Every non-formula image-bearing page made its one pre-geometry composition decision +- [x] Every image-bearing page made its one pre-geometry composition decision +- [x] Every selected formula uses the checker-valid ordinary/inline/block form with a matching visible SVG preview and no formula image resource +- [x] Every selected hyperlink uses a checker-valid inline/whole-object anchor and an exact external or same-deck target - [x] Resolved SVG pages and their project-local references exist - [x] Every role declared by an installed template spec is locatable in the finished pages, or its non-use is deliberate — checked per installed spec, not from memory - [x] Every triggered capability-specific preparation and pre-checker verification completed diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/stages/resume-execute.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/stages/resume-execute.md index 141d7aca..78bf023f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/stages/resume-execute.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/stages/resume-execute.md @@ -38,7 +38,7 @@ Verify the project's planning-session artifacts before doing anything else: | `/spec_lock.md` | Always | Strategist's execution anchors and routing contract; read it completely once in this fresh execution context | | `/design_spec.md` | Always | Complete approved design narrative and Section IX page outline; read it completely once in this fresh execution context | | `/notes/total.md` | Design Spec §X records a supplied final/literal narration script | Frozen verbatim narration input; read it once before SVG authoring and never reconstruct it from the planning chat | -| `/images/` plus files whose row status requires existence | `spec_lock images` references any image | `Existing` / `Generated` / `Sourced` / `Rendered` files must exist; an absent `Needs-Manual` file remains allowed until the Step 7 readiness gate | +| `/images/` plus files whose row status requires existence | `spec_lock images` references any image | `Existing` / `Generated` / `Sourced` files must exist; an absent `Needs-Manual` file remains allowed until the Step 7 readiness gate | | `/templates/` | `spec_lock page_layouts` references any | Layout / mirror prototypes required by execution | | Resolver-returned Chart/Table SVG | `spec_lock page_visualizations` or legacy `page_charts` references a live Chart/Table key | Shared page-local SVG selected through the two live catalogs | @@ -64,7 +64,7 @@ If any required artifact is missing, report it and stop this stage. Do not enter - Missing `design_spec.md` / `spec_lock.md` → use [`failure-recovery.md`](../governance/failure-recovery.md) §3. - Missing frozen `notes/total.md` when §X declares a final/literal script → return to Generate Step 4's prepared final narration branch; never rewrite the script from memory. -- Missing `images/`, or a file whose status requires existence → recover by provenance: an `Acquire Via: user` / `Status: Existing` file is a required manual artifact, so use `failure-recovery.md` §2 and wait for the user to restore that exact file; a template-bundled bitmap returns to [`generate-pptx`](../generate-pptx.md) Step 3 to restore the selected workspace; an AI, web, formula, or slice output uses its matching row in `failure-recovery.md` §1 to reacquire, rerender, or derive it. An absent `Needs-Manual` file is not a Step 1 failure. +- Missing `images/`, or a file whose status requires existence → recover by provenance: an `Acquire Via: user` / `Status: Existing` file is a required manual artifact, so use `failure-recovery.md` §2 and wait for the user to restore that exact file; a template-bundled bitmap returns to [`generate-pptx`](../generate-pptx.md) Step 3 to restore the selected workspace; an AI, web, or slice output uses its matching row in `failure-recovery.md` §1 to reacquire or derive it. An absent `Needs-Manual` file is not a Step 1 failure. Formula markers are SVG authoring content and never create a required image file. - Missing `templates/` inputs → restore the selected workspace through [`generate-pptx`](../generate-pptx.md) Step 3 and [`apply-template-workspace`](apply-template-workspace.md). If the workspace is unavailable or invalid, run Create Template again rather than reconstructing a template inside this stage. --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md index 80af10e7..7488e72e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/template-fill-pptx.md @@ -298,6 +298,12 @@ The script: | Adds timestamp to PPTX filename | Matches the main SVG-to-PPTX export convention | | Drops orphaned source parts | Output carries only the selected pages and the layouts / media / charts they still reference (reachability prune) | +**Hyperlink preservation**: External hyperlinks remain unchanged. A same-deck +slide jump is retargeted only when its source destination maps unambiguously to +one output slide; a self-link maps to the current clone. If the destination was +omitted or reused into multiple output slides, `apply` fails instead of linking +to an orphan or choosing a target silently. + **Animation policy**: Template-fill preserves each cloned slide's existing object animation XML (the SVG pipeline's generated object animation defaults are not applied here). It also preserves source page transitions by default. diff --git a/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json index dc40fc7c..ffa33b24 100644 --- a/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "shadcn", "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", - "commit": "a85299a9edd2a961e32f01ced86963a852652bd2", + "commit": "dec288ed71d60f6cb090f60f94bc648eaf1c528b", "adapter": "claude-skill", "sourcePath": "skills/shadcn", - "syncedAt": "2026-08-13T08:38:10Z" + "syncedAt": "2026-08-13T15:59:59Z" }