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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "caveman",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.0",
|
||||
"description": "Caveman 是面向 Codex 的精简表达、代码工作流和上下文压缩 skill 集合。",
|
||||
"author": {
|
||||
"name": "Julius Brussee",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "caveman",
|
||||
"repo": "https://github.com/JuliusBrussee/caveman.git",
|
||||
"ref": "main",
|
||||
"commit": "3b74643f4d910f496babd4e634b1ba7168816f14",
|
||||
"commit": "367fdb7f0f8f8e7994b5aab632c7ce5014802b32",
|
||||
"adapter": "codex-plugin",
|
||||
"sourcePath": "plugins/caveman",
|
||||
"syncedAt": "2026-09-02T16:00:01Z"
|
||||
"syncedAt": "2026-09-04T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ description: >
|
||||
scope. Returns caveman diff receipt. Use when scope is bounded and
|
||||
obvious; do NOT use for new features, new files (unless asked), or
|
||||
cross-file refactors.
|
||||
tools: [Read, Edit, Write, Grep, Glob]
|
||||
---
|
||||
|
||||
Caveman-ultra. Drop articles/filler. Code/paths exact, backticked. No narration.
|
||||
|
||||
@@ -5,7 +5,6 @@ description: >
|
||||
"what calls Y", "list all uses of Z", "map this directory". Output is
|
||||
caveman-compressed so the main thread eats ~60% fewer tokens than
|
||||
vanilla Explore. Refuses to suggest fixes.
|
||||
tools: [Read, Grep, Glob, Bash]
|
||||
model: haiku
|
||||
---
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ description: >
|
||||
no scope creep. Output format `path:line: <emoji> <severity>: <problem>. <fix>.`
|
||||
Use for "review this PR", "review my diff", "audit this file". Skips
|
||||
formatting nits unless they change meaning.
|
||||
tools: [Read, Grep, Bash]
|
||||
model: haiku
|
||||
---
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ Never drop not/never/no/only/except flip meaning worse than any token saved. Num
|
||||
|
||||
Never ADD word to sound caveman. Compression only style never grow output. No inserted pronoun or copula to fake broken grammar: "when it not" cost one token more than "when not" and say same thing. Keep correct verb form when correct form cost same "sees" one token, "see" one token, so mangle buy nothing and read worse. Same rule as abbreviations and arrows: if caveman phrasing not shorter than plain phrasing, use plain.
|
||||
|
||||
Clarity register: mix ASD-STE100 Simplified Technical English into caveman, always. One idea per sentence. Sentence short, target 20 words max. Active voice. Present tense where true. One word one meaning: same term for same thing every time, no synonym rotation. Instruction = imperative: "Run X", not "X should be run". Noun cluster 3 words max. Pronoun only with one clear referent, else repeat noun. Caveman cut filler; STE keep what make meaning unambiguous. Conflict between them → clarity win.
|
||||
|
||||
Tool calls: fire direct. No preamble, plan, or progress note before or between calls. After result: next call direct or final answer never announce next call. Text before call only to clarify, warn security/irreversible, or resolve ambiguity.
|
||||
|
||||
Preserve user's dominant language exactly reply in the language user writes, never switch regardless of example text or multilingual context elsewhere. Compress the style, not the language. Every emitted line in that language openings, pre-tool status lines, all not just final reply. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless user explicitly ask for translation.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "etunel-role-collaboration",
|
||||
"repo": "https://git.playones.com/chenmingxuan/EP-Hub-Skill.git",
|
||||
"ref": "main",
|
||||
"commit": "76cb0846d42e24bec29fbb3e3a39ea7f4338424e",
|
||||
"commit": "5f9951aa6e249f86ebc84b1a12ba8e9e94ba3786",
|
||||
"adapter": "claude-skill",
|
||||
"sourcePath": "etunel-role-collaboration",
|
||||
"syncedAt": "2026-09-03T02:28:39Z"
|
||||
"syncedAt": "2026-09-04T16:00:00Z"
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ description: "用于 Etunel 多 Agent 项目的角色识别、Hub 中介、依
|
||||
- 任务目标、唯一主责角色、协作角色、上游依赖和完成时间;
|
||||
- 已有资料、本阶段完成约定、要求产出的文件或成果包、最低内容、证据、完成条件和下游用途。
|
||||
|
||||
Etunel 负责当前角色和会话绑定。不要向负责人询问或复述 role ID、member ID、session ID、消息 ID 等运行时字段,也不要在任务正文中制造不存在的 ID。Hub 需要识别角色时调用 `mcp__etunel__etunel_list_roles`,用 `role` 路由、用 `display_name` 面向人显示。
|
||||
Etunel 负责当前角色和会话绑定。不要向负责人询问或复述 role ID、member ID、session ID、消息 ID 等运行时字段,也不要在任务正文中制造不存在的 ID。Hub 需要识别角色时调用 `mcp__etunel__etunel_list_roles`:用 `role` 路由,用 `display_name` 表示角色名称,用 `member_alias` 识别该角色当前实际负责人。`member_alias` 已由 Etunel 绑定确认,通知、称呼或成果确认不再额外索要人员 ID 或重复核对负责人身份。
|
||||
|
||||
## 共享硬约束
|
||||
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ Hub 定义任务产出、成员提交结果、判断状态或校验门禁时读
|
||||
|
||||
自审结果说明角色如何按任务要求检查产出,可以是“通过”“部分通过”或“未通过”,并列出依据与例外。
|
||||
|
||||
负责人确认表示当前负责人实际查看本次结果并确认可以作为该角色正式提交。至少记录确认人、时间、文件或版本、范围和附带条件。AI 不得自行声称已获确认;模拟项目只确认模拟材料的使用和流程推进,不证明模拟值真实。
|
||||
负责人确认表示当前负责人实际查看本次结果并确认可以作为该角色正式提交。Hub 以 `etunel_list_roles` 返回的 `member_alias` 作为该角色实际负责人身份,不再要求额外人员 ID 或二次身份核对;该负责人可在当前角色会话或钉钉中明确确认。至少记录确认人、时间、文件或版本、范围和附带条件。通知送达、已读或仅被 @ 不构成批准。AI 不得自行声称已获确认;模拟项目只确认模拟材料的使用和流程推进,不证明模拟值真实。
|
||||
|
||||
负责人确认随对应文件、版本、适用范围和附带条件继续有效。Hub 归档、移动或引用同一成果不需要重新确认;只有内容或版本发生实质变化、当前用途超出原确认范围、负责人发生变化、确认证据缺失,或出现与原结论冲突的新事实时,才重新确认。另一角色对该成果作新的专业评估时,仍须取得该角色自己的负责人确认。
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Hub 只检查角色、必填内容、成果、版本、证据、负责人确认
|
||||
|
||||
1. 判断输入属于当前项目还是新的独立事项;归属不明时留在业务入口澄清。
|
||||
2. 使用当前 Etunel 绑定,不从文档示例、聊天或记忆猜测角色与会话信息。
|
||||
3. 调用 `mcp__etunel__etunel_list_roles` 读取实际角色,用 `role` 路由,用 `display_name` 面向人显示;不要求负责人核对内部 ID。
|
||||
3. 调用 `mcp__etunel__etunel_list_roles` 读取实际角色,用 `role` 路由,用 `display_name` 表示角色名称,用 `member_alias` 识别当前实际负责人;不要求负责人核对内部 ID,也不重复询问负责人身份。
|
||||
4. 按 [项目文件与进度留档](project-files-and-progress.md) 建立目录、成员清单、项目概览和初始进度。
|
||||
5. 从《项目概览》读取当前项目已经确认的特殊规则;只登记有权负责人明确确认的内容,并持续沿用到同一决定权人修改或取消,不在每项任务中重复询问。
|
||||
6. 在途项目继续使用已登记流程基线,除非取得明确迁移决定。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "grill-me",
|
||||
"repo": "https://github.com/mattpocock/skills.git",
|
||||
"ref": "main",
|
||||
"commit": "6654f6b60cd9d5be8b54c6fafe44346dabeb3b76",
|
||||
"commit": "3cca18b368ae95cdbdebbff572ccafa662551015",
|
||||
"adapter": "skill-collection",
|
||||
"sourcePath": "skills/productivity",
|
||||
"syncedAt": "2026-08-24T16:00:00Z"
|
||||
"syncedAt": "2026-09-04T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"name": "playwright浏览器自动化操作",
|
||||
"version": "20260605",
|
||||
"keySource": "none",
|
||||
"syncedAt": "2026-09-03T16:02:00Z"
|
||||
"syncedAt": "2026-09-04T16:02:13Z"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "next-skills",
|
||||
"repo": "https://github.com/vercel/next.js.git",
|
||||
"ref": "canary",
|
||||
"commit": "4e2abec31d4dd8df6040136e6f680c611418236f",
|
||||
"commit": "602a2aba900e45fb2edd694f463f435430678e1f",
|
||||
"adapter": "skill-collection",
|
||||
"sourcePath": "skills",
|
||||
"syncedAt": "2026-09-03T16:00:00Z"
|
||||
"syncedAt": "2026-09-04T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "ppt-master",
|
||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||
"ref": "main",
|
||||
"commit": "6a8e91e244c987e49fbc4744be3b6d235f4f1e77",
|
||||
"commit": "8295ca1416ad17cf25ed17765024bc7ba411782c",
|
||||
"adapter": "claude-skill",
|
||||
"sourcePath": "skills/ppt-master",
|
||||
"syncedAt": "2026-09-03T09:20:38Z"
|
||||
"syncedAt": "2026-09-04T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ When one semantic object continues across adjacent slides, the destination slide
|
||||
|---|---|
|
||||
| Owner | `morph` belongs to the destination; `morph.from` is the immediately preceding stem in export order |
|
||||
| Source of pairs | `scaffold` never guesses identity — add pairs from the motion plan after inspecting final direct-root ids |
|
||||
| Pair key | A stable identity whose `from`/`to` are unique direct-root `<g>` ids on the two slides, written without `!!` (export writes the Selection Pane name `!!<key>` on both); a root primitive with a static role marker is not pairable |
|
||||
| Pair key | A stable identity whose `from`/`to` are unique direct-root `<g>` ids on the two slides, written without `!!` (export writes the Selection Pane name `!!<key>` on both); a root primitive with a static role marker is not pairable, and neither is a `Native-ready=yes` chart or table — `--native-charts-and-tables` turns it into a graphicFrame and the pair fails at export |
|
||||
| Destination effect | A destination with pairs sets `effect: morph` explicitly (`morph_by` omitted or `object`; `word`/`character` rejected; a CLI override that changes the effect fails) |
|
||||
| Chains | A middle slide may continue an object into another Morph under the same key |
|
||||
| Uniqueness | One key never names two objects on a slide; one object never carries two keys; every `!!` key shared by adjacent Morph pages must be declared |
|
||||
@@ -179,6 +179,8 @@ Morph tweens matched objects across consecutive slides, so any continuous action
|
||||
| A dropping scrim or growing cut | Progressive reveal |
|
||||
| The same wide image at two `x` offsets | Camera pan (`#C2-01`) |
|
||||
|
||||
**Reference — continuity carriers**: the Morph most decks need is not an action but a carrier — one object that persists across pages and re-seats: a progress axis or station marker that travels to the current stop, a hero figure that shrinks into the next page's header, a title band that hands one page's identity to the next. Carriers orient the reader, their endpoints are the same kind by construction, and one key chains them through a whole section (§2.1 Chains).
|
||||
|
||||
**Hard rule — matching needs compatible identity, not identical geometry**: prefer §2.1 pairs; ids and visible state may differ, but both endpoints must resolve to one compatible top-level PowerPoint object kind — a shape and a picture cross-fade instead of tweening. Automatic Morph is heuristic.
|
||||
|
||||
**Give text somewhere to come from**: text present only on the second page can only fade in — place the next page's copy just below the canvas and the previous page's just above, so blocks slide through the frame. A wholly off-canvas endpoint is one direct-root `<g id>` with valid `data-pptx-bounds` and `data-pptx-morph-staging="true"`, explicitly paired when Morph stays enabled; the marker never excuses a partially clipped group. Declare identity through the destination's `morph` block, never `data-pptx-shape-name` (importer metadata, [`svg-effects.md`](./svg-effects.md) §6.6).
|
||||
@@ -232,7 +234,7 @@ Animations anchor on unique top-level `<g id>` content groups (`cover-title`, `c
|
||||
|
||||
**Hard rule — existing groups are not custom-animation intent**: during the custom stage derive one group per logical motion unit from claims, comparisons, sequence, causality, and narration — splitting coarse wrappers and merging fragments without changing appearance, never to hit a count — and run `list-groups` only after that rewrite.
|
||||
|
||||
**Chrome stays static**: `data-pptx-layer` and explicit static role/placeholder markers are absolute. The legacy chrome-name heuristic (background, header/footer, decor, watermark, page number, nav, logo, rule) applies only to unmarked top-level groups, and only it may be overridden by a sidecar entry. Wrap logical sections in `<g id>` ([`shared-standards-core.md`](./shared-standards-core.md) §4.3); a root with no top-level group falls back to a bounded primitive set ([`svg-pipeline.md`](../scripts/docs/svg-pipeline.md)).
|
||||
**Chrome stays static by default**: a `data-pptx-layer` group is structural and never animates, and a full-canvas background rect becomes the slide background, which cannot animate either. A static role/placeholder marker or a chrome-name id (background, header/footer, decor, watermark, page number, nav, logo, rule) keeps a group out of automatic animation and the scaffold; a sidecar entry that names it animates it — a background field panning (§4.1), a cover band sliding in — judged per page, never by category. Wrap logical sections in `<g id>` ([`shared-standards-core.md`](./shared-standards-core.md) §4.3); a root with no top-level group falls back to a bounded primitive set ([`svg-pipeline.md`](../scripts/docs/svg-pipeline.md)).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ Repeating symmetric card grids without a page job is the failure mode these stru
|
||||
| Activation | Only an explicit user motion instruction, an enabled Custom Animations outcome, or an existing `animations.json`; a §IX Motion suggestion alone does not activate it |
|
||||
| Missing state | A deck exported without both states cannot gain the motion by a flag |
|
||||
| Extra page | Adding a page is a §IX roster change and returns to Strategist |
|
||||
| Endpoint id | Fix the first endpoint's direct-root `<g id>` when that page is drawn and record the intended partner id in `notes/total.md`; the next page reuses it, so no endpoint is renamed after both pages exist (a rename reruns the gate) |
|
||||
|
||||
---
|
||||
|
||||
@@ -271,7 +272,7 @@ Mechanical repetition comes from reusing one carrier and topology without a page
|
||||
- **Element grouping (Mandatory)**: wrap each logical Slide-local body unit in a descriptive, page-unique top-level `<g id>` with root-coordinate `data-pptx-bounds="x y width height"`. A helper-authored preset atom stays top-level with `data-pptx-frame` and no bounds; nested groups need none. Give a root background image or full-canvas scrim/decoration rectangle a stable `id` plus `data-pptx-role="background"` / `"decoration"` instead of a wrapper. Thresholds, exemptions, and the Morph staging marker: [`shared-standards-core.md`](./shared-standards-core.md) §4.3.
|
||||
- **Reference — nested edit groups**: a top-level group may contain descriptive nested `<g>` groups for meaningful subunits; they need no bounds and create no animation step, with no default depth or quota.
|
||||
- **Default — bounds are the module zone, not a glyph box (may skip when no text is estimable)**: make each zone as generous as the canvas and siblings allow without overlap. A group's bounds are the union of every child's geometry — text lines, preset frames, image frames, line stroke half-width — plus margin, never the title text alone. An untransformed line spans `y − 0.85 × font_size` to `y + 0.35 × font_size`. Write the sentence first, then fit the zone to it; recompute the bounds after any font-size, line-count, or child-geometry change.
|
||||
- **Width estimation**: width is estimated, never measured per page. The route calibrates every role once before P01 (Default Step 6, Quick §3) and keeps a per-role chars-per-100-px table in context; size each zone as characters ÷ that rate × 100; a line mixing CJK with Latin words or digits adds the two parts, CJK chars ÷ CJK rate + other chars ÷ Latin rate (the estimator already carries the wrapping headroom). Never trim wording to satisfy an estimate. When text does not fit, expand a zone with unused space first, then reflow or switch texture (prose → points) before dropping a qualifier; larger bounds never repair off-canvas text.
|
||||
- **Width estimation**: width is estimated, never measured per page. The route calibrates every role once before P01 (Default Step 6, Quick §3) and keeps a per-role chars-per-100-px table in context; size each zone as characters ÷ that rate × 100; a line mixing CJK with Latin words or digits adds the two parts, CJK chars ÷ CJK rate + other chars ÷ Latin rate (acronyms and uppercase words use the CAPS rate, numbers the DIGITS rate; the rates are sample averages while the checker measures real glyphs, so keep about 5% below the bounds width; a line rewritten after calibration — an expanded title, a longer label — is re-estimated the same way, the outline column covers planned wording only). Never trim wording to satisfy an estimate. When text does not fit, expand a zone with unused space first, then reflow or switch texture (prose → points) before dropping a qualifier; larger bounds never repair off-canvas text.
|
||||
- **Spec adherence**: binding color, canvas, typography, identity, resource, and template anchors hold; layout and other References apply under §2.1 without becoming locks.
|
||||
- **Template structure**: inherit the native framework only for `template_reuse_scope: mirror|layout`; `style` uses the flat route.
|
||||
- **Main-agent ownership**: the current main agent hand-writes every page SVG — never a sub-agent, and never a generator that writes slide files — because pages share upstream context for cross-page continuity. `preset_shape_svg.py` and `shape_boolean_svg.py` print fragments only after the agent has chosen role, operands, paint, and z-order. Resource, inspection, checker, verification, and export tools are unrestricted.
|
||||
|
||||
@@ -125,11 +125,11 @@ A sheet generates compatible transparent **illustration**, **illustrated-icon**,
|
||||
|
||||
**Sheet prompt convention** — one `page_role: local` item with `image_size` from final placement; spot sheets `text_policy: none`, lettering sheets `embedded`:
|
||||
|
||||
- **Grid**: derive `aspect_ratio` and `--grid` from the target shape, not a universal square grid. State an invisible logical **R×C grid** and the cell shape (compact square object, tall portrait, wide vignette, wide lettering mark); center and isolate each element with even clear gutters. Never draw cells, panels, dividers, borders, frames, or alternate gutter colors; never shrink every subject into a square sticker.
|
||||
- **Key**: one flat chroma key across the sheet — pure `#00FF00`, `#0000FF`, or `#FF0000`, chosen so its color dominates no element or effect — stated as exact HEX before the palette and the subjects (deck colors appear only inside elements), unchanged in every gutter, free of reflections or spill; grain, halftone, and vignette stay inside elements. The key is technical, not deck palette.
|
||||
- **Grid**: derive `aspect_ratio` and `--grid` from the target shape, not a universal square grid. State an invisible logical **R×C grid** and the cell shape (compact square object, tall portrait, wide vignette, wide lettering mark); center each element in about 65% of its cell and keep at least 10% key-only margin on every side — tips, steam, particles, and effects included — so `slice_images.py --strict-alpha` never meets content on a cell edge. Never draw cells, panels, dividers, borders, frames, or alternate gutter colors; never shrink every subject into a square sticker.
|
||||
- **Key**: one flat chroma key across the sheet — pure `#00FF00`, `#0000FF`, or `#FF0000`, chosen so its color dominates no element or effect — stated as exact HEX before the palette and the subjects (deck colors appear only inside elements), unchanged in every gutter, free of reflections or spill; grain, halftone, and vignette stay inside elements. The key is technical, not deck palette. Every pixel that is not an element is the key itself — no white card, panel, paper, mat, or frame under an element and no second tint of the key; state the key rule first in the prompt and repeat it last, because a model told only "no borders" still paints each element on a white card.
|
||||
- **Identity**: shared `deck_rendering` + `color_scheme`.
|
||||
- **Illustration / illustrated-icon sheet**: name each element and its page or reuse job; for an icon, the compact cue that must survive at placement size; the §5.3 `none` cue.
|
||||
- **Lettering sheet**: exactly one named stable string per cell as the only text, quoted literally; the group's letterform character and treatment, then role, placement/background relationship, relative weight, and energy under §5.3's controlled-authorship default; artistry glyph-bound (silhouette, stroke structure, material, texture, depth, contour-bound light). No topic motifs, scene fragments, icons, detached ribbons, or particles unless the approved treatment is a lettering-plus-illustration lockup; key-only padding, no scene, unrelated copy, labels, watermark, or mockup surface.
|
||||
- **Lettering sheet**: exactly one named stable string per cell as the only text, quoted literally; the group's letterform character and treatment, then role, placement/background relationship, relative weight, and energy under §5.3's controlled-authorship default; artistry glyph-bound (silhouette, stroke structure, material, texture, depth, contour-bound light). No topic motifs, scene fragments, icons, detached ribbons, or particles unless the approved treatment is a lettering-plus-illustration lockup; key-only padding, no scene, unrelated copy, labels, watermark, or mockup surface. A single mark on a wide sheet (3:2 or wider) tends to come back with a faded ghost copy of itself above or below the real mark — give a lone mark a narrow band (about 4:1) that leaves no room for a second line, and inspect every lettering slice for a ghost band, not only for the characters.
|
||||
- **Delivery floor, not an aesthetic ceiling**: enlarge the cell, change the grid, or use a larger or separate sheet when a treatment needs footprint; never weaken an approved treatment to fit a crop.
|
||||
|
||||
**Cell geometry is designed**: `slice_images.py --grid RxC` cuts rows first; `cell_ratio = sheet_ratio × rows / cols`. On a wide sheet `1xN` yields tall cells and `Nx1` wide cells; any `MxN` is valid when its cells match the placements.
|
||||
@@ -162,7 +162,7 @@ python3 scripts/slice_images.py <project>/images/illus_sheet.png --grid 2x3 \
|
||||
|
||||
| Constraint | Rule |
|
||||
|---|---|
|
||||
| **Strict key recovery** | Measure the smallest channel distance from any element pixel to the key first; raise `--tolerance` only enough to absorb measured flat-field drift and keep it below that distance, `--inset` only for an isolated outer gutter, and regenerate or enlarge when an effect reaches an edge. A `1x1` lettering sheet whose glyph touches the outer key border fails the same gate: pad the sheet with a key-colored border (about 10%) before slicing, or regenerate with more margin |
|
||||
| **Strict key recovery** | Measure the smallest channel distance from any element pixel to the key first; raise `--tolerance` only enough to absorb measured flat-field drift and keep it below that distance, `--inset` for an isolated outer gutter or for grid lines the model drew between cells (its most frequent use — trim the gutter, never an element), and regenerate or enlarge when an effect reaches an edge. A `1x1` lettering sheet whose glyph touches the outer key border fails the same gate: pad the sheet with a key-colored border (about 10%) before slicing, or regenerate with more margin |
|
||||
| **Clean isolated cells** | Fused cells, scene backgrounds, or flourishes crossing a cell make the sheet unusable; re-rolls follow only a strict keying failure or user/preview evidence, never taste |
|
||||
| **Enough source pixels** | Each cell at least 1.5–2× its display size (`1K` small accents, `2K` medium, `4K` large or enlarged) |
|
||||
|
||||
@@ -237,7 +237,7 @@ Match facial detail, anatomy, texture, and realism to the rendering and the row'
|
||||
|
||||
**Hard rule — decide by editability, not model capability**: Layer 1 text can never be edited, corrected, searched, restyled, or reflowed. Text that is part of the artwork and stable — decorative lettering, a wordmark, a hand-lettered phrase, figure-internal identifiers — may be Layer 1. Anything that must stay exact, searchable, editable, or may be reworded is Layer 2, whatever `text_policy` says: authoritative titles, chrome, navigation, footer, bullets, captions, data values. Bake title-like wording only when the approved plan treats those exact characters as stable artwork. When the headline must stay editable, use Primitive D and overlay it.
|
||||
|
||||
**Hard rule — never pre-judge by script or length**: never push text to SVG, shorten a headline, or downgrade `embedded` to `none` on the assumption that a script or long string "won't render"; a multi-word phrase or two-line lockup qualifies exactly as one word does. Name the exact characters literally; do not re-read the generated image to verify them. Exception — CJK lettering: when `text_policy: embedded` and the string contains CJK characters, one look at the generated file is allowed, answering only whether the characters match the approved string exactly; a mismatch regenerates that item, and the look never reopens selection or taste.
|
||||
**Hard rule — never pre-judge by script or length**: never push text to SVG, shorten a headline, or downgrade `embedded` to `none` on the assumption that a script or long string "won't render"; a multi-word phrase or two-line lockup qualifies exactly as one word does. Name the exact characters literally; do not re-read the generated image to verify them. Exception — CJK lettering: when `text_policy: embedded` and the string contains CJK characters, one look at the generated file is allowed, answering only whether the characters match the approved string exactly; a mismatch regenerates that item, and the look never reopens selection or taste; after three regenerations that still miss the same character, stop — keep that mark as native text, record the fallback in the notes, and never explain it on the page.
|
||||
|
||||
**Reference — controlled, deck-aligned artistic authorship** (high expression on user request or a confirmed direction): give the model the exact string, communication role, placement/background relationship, deck identity, relative weight, and desired energy; the rendering, semantic colors, mood, and page hierarchy define the envelope. Glyph-native expression carries identity through silhouette, stroke construction, internal material/texture, contour-bound depth and light, and composition. Literal topic illustrations or detached decoration compete with the glyph; a lettering-plus-illustration lockup needs an explicit request or confirmed direction. Within the treatment let the model combine or omit gesture, material, dimensionality, texture, lighting, and hierarchy — possibility space, not a recipe. Never flatten the art to ease extraction (§4.3's gates protect delivery); when fit is uncertain use the lower density; keep a multi-line lockup as one element when its hierarchy is part of the art.
|
||||
|
||||
|
||||
+2
-2
@@ -72,8 +72,8 @@ Every payload carries that exact `schema`; `columns` holds the optional header r
|
||||
|
||||
### Chart schemas
|
||||
|
||||
Supported `type` values: `column`, `bar`, `line`, `area` (each with `grouping`), `pie`, `doughnut`, `pieOfPie`, `barOfPie`, `radar`, `radarMarkers`, `radarFilled`, `scatter`, `bubble`, `combo`, `treemap`, `sunburst`, `histogram`, `pareto`, `boxWhisker`, `waterfall`, `funnel`, `stock`. Category charts take `categories` plus `series[].values`; XY charts take `series[].x` / `y` (bubble adds `size`); combo takes `plots[]` or typed `series[]`; ChartEx types take their own hierarchy or value fields. `data_labels`, `axes`, `axis_titles`, `show_legend` / `legend_position`, companion text (`caption`, `source`, `note`), and `style.colors` are the chrome fields. 3D aliases and `surface` are unsupported; exploded pie, `map`, `heatmap`, `bullet`, and `gantt` fail fast. Every field, enum, and range: [`native-data.md`](../scripts/docs/native-data.md).
|
||||
Supported `type` values: `column`, `bar`, `line`, `area` (each with `grouping`), `pie`, `doughnut`, `pieOfPie`, `barOfPie`, `radar`, `radarMarkers`, `radarFilled`, `scatter`, `bubble`, `combo`, `treemap`, `sunburst`, `histogram`, `pareto`, `boxWhisker`, `waterfall`, `funnel`, `stock`. Category charts take `categories` plus `series[].values`; XY charts take `series[].x` / `y` (bubble adds `size`); combo takes `plots[]` or typed `series[]`; ChartEx types take their own hierarchy or value fields. `data_labels` (column / bar / line / area plots only; other types show values through companion text), `axes`, `axis_titles`, `show_legend` / `legend_position`, companion text (`caption`, `source`, `note`), and `style.colors` are the chrome fields. 3D aliases and `surface` are unsupported; exploded pie, `map`, `heatmap`, `bullet`, and `gantt` fail fast. Every field, enum, and range: [`native-data.md`](../scripts/docs/native-data.md).
|
||||
|
||||
**Chart chrome and color**: SVG-first metadata matches the fallback chrome and reads it literally — the dominant axis/grid stroke becomes `style.axis_color`, numbers keep their written form, and marker text that is not a category, data label, axis label, or legend entry needs a companion entry; JSON-first owns its chrome. Generated payloads use uppercase `#RRGGBB`. Negative bars keep the series fill.
|
||||
**Chart chrome and color**: SVG-first metadata matches the fallback chrome and reads it literally — the dominant stroke of elements named `axis` becomes `style.axis_color` and of those named `grid` becomes `style.grid_color` (unlabeled lines fall back to the marker's dominant stroke), numbers keep their written form (a payload `73.0` renders as `73` unless `number_format` says otherwise), and marker text that is not a category, data label, axis label, or legend entry needs a companion entry; JSON-first owns its chrome. Generated payloads use uppercase `#RRGGBB`. Negative bars keep the series fill. The final checker tags SVG-first parity findings `blocks --native-charts-and-tables export`; that export refuses them, so clear them before exporting the native variant.
|
||||
|
||||
**Forbidden — replacement marker transforms**: no rotate, skew, or matrix on table/chart groups; translate/scale only, because native frames cannot carry arbitrary SVG transforms.
|
||||
|
||||
@@ -28,7 +28,7 @@ Authoring contract for PowerPoint-native click hyperlinks on complete objects an
|
||||
|
||||
**Hard rule — one target syntax**: author SVG 2 `href` (import may read legacy `xlink:href`; generated SVG never writes both). Same-deck destinations use the exact `#slide-N` form inside the final roster. External destinations are absolute URIs with an explicit scheme and percent-encoded spaces; relative paths, arbitrary fragments, filesystem paths, and `data:` / `file:` / `javascript:` / `vbscript:` fail closed.
|
||||
|
||||
**Hard rule — inline run**: visible text sits in one or more `<tspan>` children inside the anchor; the anchor and its descendants own no `x`, `y`, `dx`, or `dy` — line positioning belongs to the enclosing line `<tspan>`. A linked inline formula is one leaf formula `<tspan>` inside the anchor and keeps its native math contract.
|
||||
**Hard rule — inline run**: visible text sits in one or more `<tspan>` children inside the anchor; the anchor and its descendants own no `x`, `y`, `dx`, or `dy` — line positioning belongs to the enclosing line `<tspan>` — in a multi-line paragraph the first line is no exception: its anchor also sits inside a line `<tspan>`, never in direct `<text>` content. A linked inline formula is one leaf formula `<tspan>` inside the anchor and keeps its native math contract.
|
||||
|
||||
**Hard rule — whole-object hit area**: wrap at least one visible SVG element; no direct text or bare `<tspan>` 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 animation may target an outer top-level `<g>`, but a hyperlink-bearing group cannot also be an interactive `trigger_shape` — one click has one owner.
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ python3 skills/ppt-master/scripts/visualization_recall.py validate \
|
||||
|
||||
**Family selection**: user/template typography is authoritative. Delivery target: an explicit user/template target first, otherwise Windows Microsoft PowerPoint (owner: [`shared-standards-core.md`](./shared-standards-core.md) §4.1) — the authoring host's installed fonts never select a face; name concrete faces installed or approved on that target; at most four families; a brand/web face leads only after user-confirmed installation, otherwise export a safe face and keep it as a Design Spec reference (fonts are not embedded; CSS tails are preview aids, not PowerPoint fallbacks). Avoid near-equivalent splits (YaHei↔PingFang, SimSun↔Songti, Arial↔Helvetica↔Segoe UI, Times↔Times New Roman). Fonts in one deck form contrast (different family, weight, or proportion) or concord (one family throughout); across the direction set include both a concord and a contrast pairing unless the user or template fixes the stack, and never default to title = body without a reason.
|
||||
|
||||
**Reference — PPT-safe faces (recall, not a whitelist; one concrete named face per script — a stack carries at most one Latin face and one CJK face and never a fallback list of alternatives, because export takes the first named face of each script)**: CJK sans `Microsoft YaHei` / `SimHei`, CJK serif `SimSun` / `FangSong` / `KaiTi` (their macOS counterparts `PingFang SC` / `Heiti SC` / `Songti SC` are preview aliases, never the named face), Latin sans `Arial` / `Calibri` / `Segoe UI` / `Verdana` / `Trebuchet MS`, Latin serif `Times New Roman` / `Georgia` / `Cambria` / `Palatino` / `Garamond`, mono `Consolas` / `Courier New`, display `Impact` / `Arial Black`. Let the locked style's character pick the axis and lead the title — `Microsoft YaHei` / `Arial` are the neutral members, never the automatic lead; a neutral sans title where the style asks for character is the failure to avoid. Non-pre-installed directions — retro/pixel Press Start 2P / VT323, rounded Nunito / Quicksand / OPPO Sans (safe substitute `Trebuchet MS` / `Verdana`), modern web Inter / HarmonyOS Sans / Source Han, calligraphic 隶书 / 华文行楷 / 华文新魏 (safe substitute `KaiTi` / `FangSong`, titles only), brand faces — need target installation or stay Design Spec references.
|
||||
**Reference — PPT-safe faces (recall, not a whitelist; one concrete named face per script — a stack carries at most one Latin face and one CJK face and never a fallback list of alternatives, because export takes the first named face of each script)**: CJK sans `Microsoft YaHei` / `SimHei`, CJK serif `SimSun` / `FangSong` / `KaiTi` (their macOS counterparts `PingFang SC` / `Heiti SC` / `Songti SC` are preview aliases, never the named face), Latin sans `Arial` / `Calibri` / `Segoe UI` / `Verdana` / `Trebuchet MS`, Latin serif `Times New Roman` / `Georgia` / `Cambria` / `Palatino` / `Garamond` (`Georgia` sets old-style figures whose digits sit at different heights — a hero-number or data role needs lining figures: `Times New Roman`, `Cambria`, or any listed sans or mono), mono `Consolas` / `Courier New`, display `Impact` / `Arial Black`. Let the locked style's character pick the axis and lead the title — `Microsoft YaHei` / `Arial` are the neutral members, never the automatic lead; a neutral sans title where the style asks for character is the failure to avoid. Non-pre-installed directions — retro/pixel Press Start 2P / VT323, rounded Nunito / Quicksand / OPPO Sans (safe substitute `Trebuchet MS` / `Verdana`), modern web Inter / HarmonyOS Sans / Source Han, calligraphic 隶书 / 华文行楷 / 华文新魏 (safe substitute `KaiTi` / `FangSong`, titles only), brand faces — need target installation or stay Design Spec references.
|
||||
|
||||
**Role extension after confirmation**: while composing the roster, add a lowercase snake_case role with an exact stack only for a recurring role that materially needs a different family (`annotation`, `footer`, `footnote`, `data`, `emphasis`, `quote`, `code`), coherent with the confirmed heading/body system and locked style; one-off garnish stays omitted and Default names any added role in one compact `Role rationale` line in §IV.
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ still be the best result.
|
||||
|
||||
**When to run**: run `preset_shape_svg.py describe <name> --compact` only when
|
||||
an exact candidate needs objective adjustment, connector, path,
|
||||
connection-site, or text rectangle facts. Authoring syntax and fragment
|
||||
connection-site, or text rectangle facts (`--frame X Y W H` adds the text rectangle in page px). Authoring syntax and fragment
|
||||
contracts remain in
|
||||
[`native-shape-authoring.md`](./native-shape-authoring.md).
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Bloomberg / Economist news-infographic — publication-grade information density
|
||||
|
||||
## 2. Typography character
|
||||
|
||||
- Serif headline / hero-number for authority × a clean sans or monospace for numeric precision in tables and chart labels. Small captions and source lines; tight, deliberate hierarchy.
|
||||
- Serif headline / hero-number for authority × a clean sans or monospace with lining figures for numeric precision in tables and chart labels. Small captions and source lines; tight, deliberate hierarchy.
|
||||
|
||||
> Families are chosen at confirmation `g`; this style asks for a serif-headline × precise-sans/mono-data *character*. This governs editable native text; decorative-lettering eligibility remains a separate carrier decision.
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@ Every payload carries that exact `schema`. Native tables are rectangular grids:
|
||||
|
||||
Cells accept `text`, `fill`, `fill_opacity`, `color`, `align` (`l` / `ctr` / `r`), `valign` (`top` / `middle` / `bottom`), `bold`, `font_size`, `padding` and side-specific `padding_*`, `border_color`, `border_width`, `borders`, `lang`, `anchor_center`, `horizontal_overflow`. Multi-paragraph text replaces `text` with a non-empty `paragraphs` list of strings or objects — empty paragraph strings are preserved as blank lines — (`align` plus exactly one of `text` or non-empty `runs`; runs carry required `text` and optional `bold`, `italic`, `underline`, `strike`, `color`, `font_size`, one-typeface `font_family`, `lang`, `alt_lang`); unknown fields, wrong types, empty runs, multi-typeface families, and unsupported colors fail. Per-side borders use `borders.left|right|top|bottom|diagonal_down|diagonal_up` as `{ "style": "none" }` or `{ "style": "solid", "color": "#RRGGBB", "width": <positive-px> }`, overriding a uniform `border_color` / `border_width` on style or cell. A missing `lang` derives `zh-CN` for CJK and `en-US` otherwise. `style.band_row: false` disables banding and materialized alternating fills. Typography mirrors the fallback: `style.font_family` and `style.font_size` from the drawn table text, `style.header_font_size` or per-cell `font_size` only where the fallback differs; with no explicit table font, Default uses the deck body family and anchor, Quick its active-context values.
|
||||
|
||||
`style` accepts `font_family`, `font_size`, `header_font_size`, `band_row`, `padding`, `valign`, `border_color`, `border_width`, `lang`, `table_style_id`, and the table-wide colours `header_fill`, `header_text`, `body_fill`, `body_text`, `band_fill`. Fills are only what the payload resolves: a cell with no `fill` takes `style.header_fill` (header rows) or `style.band_fill` / `style.body_fill` (body rows), and when none of those is set it exports as `noFill` so the slide background shows through — the same as a fallback that draws no cell rect. Text colour never falls to nothing: a plain-text cell without `color` takes `defaults.run.color` (plain text is one run, so `defaults.run` `bold` / `color` / `font_size` reach it when the cell and `defaults.cell` leave them unset), then `style.body_text` / `style.header_text`, then `#1F2937` (`header_text` defaults to `#FFFFFF` only when `header_fill` is set). SVG-first parity reports a body text colour drawn in the fallback that none of those layers would carry.
|
||||
|
||||
**Hard rule — the table payload is complete**: a payload holding only `font_size` and a uniform border is not complete when the fallback draws a header band, row or column fills, first-column emphasis, non-uniform row heights, or sparse rules — every row, summary line, value, and cell style that must survive `--native-charts-and-tables` is in `columns` / `rows`, because fallback text is discarded on that route. Numeric or currency columns use cell objects with `align: "r"` (`text-anchor="end"` does not carry). **Merged cells — canonical rectangular contract only**: positive integer `row_span` / `col_span` on the anchor, every covered cell blank as `{"merge_continuation": true}` (a bare `{"text": ""}` is blank only while no `defaults.cell` adds fields), spans inside the grid and non-overlapping; the exporter writes `rowSpan` / `gridSpan` / `hMerge` / `vMerge`. CamelCase aliases, raw OOXML merge fields, top-level merge lists, nonblank covered cells, invalid spans, and overlaps fail.
|
||||
|
||||
## §2 Chart schemas
|
||||
|
||||
- **Category charts** — `column`, `bar`, `line`, `area`, `pie`, `doughnut`, `pieOfPie`, `barOfPie`, `radar`: `categories` plus `series[].values`. Pie-family charts take exactly one series with per-slice colors; `hole_size` is doughnut-only, integer `10..90`, default `75`; no rotation field. Column/bar may set `series[].point_colors` (camelCase `pointColors` is read-compatible; length = values). `data_labels` is `true` or an object with `show_value`, `position`, `number_format`, `font_size`, `font_family`, `bold`, `color`, per-point `colors`, and `points` (zero-based `idx` plus optional overrides); positions: clustered column/bar `outside_end` / `inside_end` / `inside_base` / `center`, stacked `inside_end` / `inside_base` / `center`, line `above` / `center` / `best_fit`, area none.
|
||||
- **Category charts** — `column`, `bar`, `line`, `area`, `pie`, `doughnut`, `pieOfPie`, `barOfPie`, `radar`: `categories` plus `series[].values`. Pie-family charts take exactly one series with per-slice colors; `hole_size` is doughnut-only, integer `10..90`, default `75`; no rotation field. Column/bar may set `series[].point_colors` (camelCase `pointColors` is read-compatible; length = values). `data_labels` (column, bar, line, and area plots only, combo plots of those types included; pie-family, radar, scatter/bubble, stock, and ChartEx charts reject it — show their values through companion `notes`) is `true` or an object with `show_value`, `position`, `number_format`, `font_size`, `font_family`, `bold`, `color`, per-point `colors`, and `points` (zero-based `idx` plus optional overrides); positions: clustered column/bar `outside_end` / `inside_end` / `inside_base` / `center`, stacked `inside_end` / `inside_base` / `center`, line `above` / `center` / `best_fit`, area none.
|
||||
- **Combo** — shared `categories` plus `plots[]` (`type: "column" | "line" | "area"`, own `series`, optional `axis: "secondary"`, optionally own `categories` / `category_numeric` when caches genuinely differ, and `series_indices` for imported identity — same-length unique non-negative integers forming one contiguous `0..N-1` range across plots) or typed `series[]` with per-series `type` / `axis` (adjacent compatible series share a plot). Area series may set `fill_opacity` (`0..1`; `fillOpacity` read-compatible); a line plot with `area_fill: true` exports as an area chart; line/area series may set `line_width` in SVG px (`lineWidth` read-compatible). Export layers areas below columns and lines.
|
||||
- **Axes** — classic `axes` is a closed object of `category`, `value`, `secondary_category`, `secondary_value`, each with only `kind` (`text` / `date` / `value`), `position`, `visible`, `label_position` (`next_to` / `none` / `low` / `high`), `number_format`, `minimum`, `maximum`, `major_unit` (value axes), `reverse`, `major_gridlines`; single-plot `bar` takes `category` left/right and `value` bottom/top; pie-family rejects `axes`. `scatter` / `bubble` use `x` and `y` roles only (`kind: "value"`; `x.position` bottom/top, `y.position` left/right) with the same fields. Logarithmic scales, minor units/gridlines, crossing values, display units, and tick skipping are unsupported.
|
||||
- **XY** — `scatter` and `bubble` use `series[].x` + `series[].y` (`bubble` adds one `series[].size` / `sizes` per point), or `series[].points` as `[x, y]` / `[x, y, size]` tuples or `{x, y, size}` objects.
|
||||
@@ -37,4 +39,4 @@ Cells accept `text`, `fill`, `fill_opacity`, `color`, `align` (`l` / `ctr` / `r`
|
||||
|
||||
## §2 Chart chrome, typography, and color
|
||||
|
||||
SVG-first metadata matches the fallback chrome; JSON-first owns it. Metadata sizes use SVG px (`1px = 0.75pt`); `style.font_family` and `title_font_size`, `subtitle_font_size`, `axis_font_size`, `axis_title_font_size`, `legend_font_size`, `note_font_size` are required only when native must preserve typography an SVG-first fallback cannot supply unambiguously (JSON-first never infers from its preview). A string or unbounded-object `title` becomes native `c:title` (`subtitle` line two); a title object with complete `x`, `y`, `width`, `height` becomes a companion text box (partial bounds or `subtitle` fail); `name` names the object; `title`, `subtitle`, and axis-title objects accept `text`, `font_size`, `font_family`, `color`; the checker rejects SVG-first title/axis text absent from the fallback. Axis titles are explicit via `axis_titles` (`category`, `value`, `x`, `y`, `secondary_value`) or the root aliases `category_axis_title`, `value_axis_title`, `x_axis_title`, `y_axis_title`, `secondary_value_axis_title`; `show_value_axis_labels: false` hides numeric tick labels (e.g. a radar without radial coordinates); native legends are opt-in via `show_legend: true` and `legend_position` (`bottom` default; `top` / `left` / `right`). SVG-first parity reads the fallback literally — `style.axis_color` equals the dominant axis/grid stroke, numbers match in written form (`286.20` ≠ `286.2`), and marker text that is not a category, data label, axis label, or legend entry needs a companion entry. Companion text (`caption`, `source`, `note`, `notes`, `footnote`, `footnotes`) exports as editable text boxes — strings or objects with `text`, `x`, `y`, `width`, `height` (slide coordinates), `font_size`, `color`, `align`, `bold`; use it for captions, sources, center labels, and annotations, and `data_labels` for point values. `style.colors` sets series colors (treemap/sunburst tile palette in order); the exporter writes explicit chart-area fill, plot-area fill, axis, gridline, and label colors (SVG-first infers them from the largest panel `<rect>`, text, and strokes; JSON-first from JSON or stable defaults), overridable under `style` with `chart_area_fill`, `plot_area_fill`, `text_color`, `axis_color`, `grid_color` (`"none"` for transparent); generated payloads use uppercase `#RRGGBB`, with `#RGB`, `rgb()`, and CSS names normalized. Negative bars keep the series fill.
|
||||
SVG-first metadata matches the fallback chrome; JSON-first owns it. Metadata sizes use SVG px (`1px = 0.75pt`); `style.font_family` and `title_font_size`, `subtitle_font_size`, `axis_font_size`, `axis_title_font_size`, `legend_font_size`, `note_font_size` are required only when native must preserve typography an SVG-first fallback cannot supply unambiguously (JSON-first never infers from its preview). A string or unbounded-object `title` becomes native `c:title` (`subtitle` line two); a title object with complete `x`, `y`, `width`, `height` becomes a companion text box (partial bounds or `subtitle` fail); `name` names the object; `title`, `subtitle`, and axis-title objects accept `text`, `font_size`, `font_family`, `color`; the checker rejects SVG-first title/axis text absent from the fallback. Axis titles are explicit via `axis_titles` (`category`, `value`, `x`, `y`, `secondary_value`) or the root aliases `category_axis_title`, `value_axis_title`, `x_axis_title`, `y_axis_title`, `secondary_value_axis_title`; `show_value_axis_labels: false` hides numeric tick labels (e.g. a radar without radial coordinates); native legends are opt-in via `show_legend: true` and `legend_position` (`bottom` default; `top` / `left` / `right`). SVG-first parity reads the fallback literally — `style.axis_color` is the dominant stroke among elements whose id or class names `axis`, `style.grid_color` the dominant stroke among those naming `grid` / `gridline`, and a role with no labeled stroke falls back to the marker's dominant stroke (axis and grid lines in different colors need those labels or one steals the other's dominant); numbers match in written form (`286.20` ≠ `286.2`; a payload `73.0` is stored as `73` and renders as `73` under General, so a fallback that shows `73.0` needs `number_format: "0.0"`; a `number_format` on `data_labels` or an axis makes its rendered form, such as `28.0` for `0.0`, an accepted variant), and marker text that is not a category, data label, axis label, or legend entry needs a companion entry. Companion text (`caption`, `source`, `note`, `notes`, `footnote`, `footnotes`) exports as editable text boxes — strings or objects with `text`, `x`, `y`, `width`, `height` (slide coordinates), `font_size`, `color`, `align`, `bold`; use it for captions, sources, center labels, and annotations, and `data_labels` for point values. `style.colors` sets series colors (treemap/sunburst tile palette in order); the exporter writes explicit chart-area fill, plot-area fill, axis, gridline, and label colors (SVG-first infers them from the largest panel `<rect>`, text, and strokes; JSON-first from JSON or stable defaults), overridable under `style` with `chart_area_fill`, `plot_area_fill`, `text_color`, `axis_color`, `grid_color` (`"none"` for transparent); generated payloads use uppercase `#RRGGBB`, with `#RGB`, `rgb()`, and CSS names normalized. Negative bars keep the series fill. The final checker reports every SVG-first parity finding as a warning tagged `(blocks --native-charts-and-tables export)`; that export refuses the same findings as errors, so clear them before exporting the native variant.
|
||||
|
||||
@@ -41,7 +41,7 @@ One resolved row contains these fields:
|
||||
| Trigger shape | Optional different top-level group; maps to PowerPoint `On Click of` |
|
||||
| Duration | Finite positive schedule duration; scalable native behavior trees preserve their internal timing ratios |
|
||||
| Delay | Finite non-negative row offset; shape-trigger rows use it as `TriggerDelayTime` |
|
||||
| Order | Positive integer sidecar order; ties retain stable SVG group order, then `effects[]` index |
|
||||
| Order | Positive integer sidecar order; ties retain stable SVG group order, then `effects[]` index; a group with no sidecar `order` follows the nearest listed group before it in SVG order (and precedes every listed group when none precedes it), so a headline above the numbered body enters first |
|
||||
| Effect options | Effect-specific `direction`, `amount`, `color`, `font_name` (one installed PowerPoint face, required for Change Font; not a CSS list), `relative`, or `size` values from PowerPoint `EffectParameters` |
|
||||
| Timing options | Repeat count/span, auto-reverse, rewind, accelerate/decelerate, bounce-end ratio, and restart policy |
|
||||
| Completion / cue | Optional dim/hide behavior and packaged `.m4a`/`.mp3`/`.wav` sound |
|
||||
@@ -301,7 +301,7 @@ animation-to-video contract.
|
||||
| `groups.<id>.effects[]` | Non-empty ordered array for a multi-duty lifecycle; every row names `effect`; cannot coexist with legacy single-effect fields in the same group |
|
||||
| `groups.<id>.effect` | Backward-compatible single-row form; old short names are read-only compatibility inputs |
|
||||
| `effects[].trigger` / legacy `trigger` | Row-specific Start mode; omitted values inherit `animation.trigger` |
|
||||
| `order` | Page-wide order for ordinary rows; ties keep SVG group order, then `effects[]` index; `trigger_shape` rows keep relative order in separate interactive sequences; SVG layer order never changes |
|
||||
| `order` | Page-wide order for ordinary rows; ties keep SVG group order, then `effects[]` index; an unlisted animated group inherits the order of the nearest listed group above it in SVG order (0 before the first); `trigger_shape` rows keep relative order in separate interactive sequences; SVG layer order never changes |
|
||||
| `delay` | Row-specific seconds added to the resolved Start or shape trigger |
|
||||
| `duration` | Per-row schedule duration; scalable native trees keep internal ratios, while `entrance_appear` and instantaneous presets keep their authored duration and use the value for `after-previous` spacing |
|
||||
| `effect_options` | Effect-specific parameters (`direction`, `amount`, `color`, `font_name`, `relative`, `size`) limited to what the selected effect supports (`pptx_animations.py --describe <effect>`); requires an explicit canonical `effect` in the same block or row; `font_name` is one target-installed face |
|
||||
@@ -313,4 +313,4 @@ animation-to-video contract.
|
||||
| `after_effect` | `none`, `dim` with `color`, `hide`, or `hide-on-next-click` |
|
||||
| `sound` | Object-animation cue: project-relative or absolute `.m4a` / `.mp3` / `.wav` on low-level inputs; bundled selections use the synced project-relative `.wav` path |
|
||||
|
||||
An unlisted SVG inherits the resolved deck-wide settings; a listed slide may contain only the `transition`, `animation`, `groups`, or `morph` fields it overrides; chrome groups (`bg` / `*-header` / `*-footer` / `*-decor` / `nav` / `watermark` / `logo` / `pagenumber`) are pinned to `none` unless explicitly named without a structural marker — except a `*-header` / `*-footer` group whose text reaches the page's median font size, which is read as the title block and stays an ordinary target; a group carrying `data-pptx-layer` or a static role/placeholder marker never animates.
|
||||
An unlisted SVG inherits the resolved deck-wide settings; a listed slide may contain only the `transition`, `animation`, `groups`, or `morph` fields it overrides; chrome groups (an id equal to, or holding a `-`/`_`-separated token from, `bg`, `background`, `header`, `footer`, `decor`, `decoration`, `decorations`, `chrome`, `nav`, `watermark`, `logo`, `pagenumber`, `pagenum`, `slidenumber`, `slidenum`, `rule`; `list-groups` names the excluded ids at the end of each slide line) and static role/placeholder groups are pinned to `none` unless explicitly named; a `data-pptx-layer` group never animates, and a full-canvas background rect becomes the slide background rather than a shape — except a `*-header` / `*-footer` group whose text reaches the page's median font size, which is read as the title block and stays an ordinary target; a group carrying `data-pptx-layer` or a static role/placeholder marker never animates.
|
||||
|
||||
@@ -22,6 +22,9 @@ python3 scripts/project_manager.py page-context-report <project_path>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Local sources under `projects/` are moved into the target project unless
|
||||
`--copy` is passed; a file inside another project's tree is copied unless
|
||||
`--move` is explicit, so borrowing a finished project's slices never empties it.
|
||||
- `--format` is optional and accepts registered canvas keys only. Pass it only
|
||||
when the actual canvas exactly matches a registered format.
|
||||
- Without `--format`, `init` creates `<name>_<YYYYMMDD>`; a name that already
|
||||
|
||||
@@ -1025,8 +1025,14 @@ the SVG quality checker.
|
||||
`validation/text_calibration.json`, and prints a compact table or JSON. The
|
||||
estimator is additive across scripts, so a line mixing CJK with Latin words
|
||||
or digits is estimated as (CJK chars ÷ CJK rate + other chars ÷ Latin rate)
|
||||
× 100; spaces, digits, and punctuation count as Latin. Rates are rounded, so
|
||||
the estimate carries a few percent of slack; leave that margin in the zone. The checker's overflow
|
||||
× 100; spaces and punctuation count as Latin, digits use the DIGITS rate.
|
||||
The rates are sample averages taken with the checker's own estimator
|
||||
(headroom included), while the checker measures each real line glyph by
|
||||
glyph: capital-heavy words, digits, and wide letters run wider than the Latin
|
||||
rate, so the table also prints CAPS and DIGITS rates, and a zone should stay
|
||||
about 5% below its bounds width. `--outline` adds the longest §IX planned
|
||||
line per role — the planned wording only; a line rewritten while authoring
|
||||
is re-estimated with the rates. The checker's overflow
|
||||
diagnostic prints that line's average px per character, which is not a
|
||||
reusable rate. A lock role without its own
|
||||
`<role>_family` resolves to `title_family` when the role name contains
|
||||
|
||||
@@ -32,7 +32,7 @@ from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
from console_encoding import configure_utf8_stdio
|
||||
from pptx_shapes import CONNECTOR_PRESET_TYPES, get_preset_registry
|
||||
from pptx_shapes import CONNECTOR_PRESET_TYPES, FormulaEvaluationError, get_preset_registry
|
||||
from pptx_shapes.semantics import get_preset_shape_semantics
|
||||
from pptx_to_svg.preset_authoring import render_preset_shape_fragment
|
||||
|
||||
@@ -101,6 +101,23 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"Print one flat objective identity view with key geometry facts."
|
||||
),
|
||||
)
|
||||
describe_parser.add_argument(
|
||||
"--frame",
|
||||
nargs=4,
|
||||
type=float,
|
||||
metavar=("X", "Y", "W", "H"),
|
||||
help=(
|
||||
"Also evaluate the text rectangle for this page frame and print it "
|
||||
"as text_rectangle_px (page coordinates, same as render --frame)."
|
||||
),
|
||||
)
|
||||
describe_parser.add_argument(
|
||||
"--adjust",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="NAME=FORMULA",
|
||||
help="Adjustment guide used when evaluating --frame (repeatable).",
|
||||
)
|
||||
|
||||
render_parser = subparsers.add_parser(
|
||||
"render",
|
||||
@@ -237,6 +254,37 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
path_count = len(definition.paths)
|
||||
connection_site_count = len(definition.connections)
|
||||
has_text_rectangle = definition.text_rectangle is not None
|
||||
text_rectangle = (
|
||||
None
|
||||
if definition.text_rectangle is None
|
||||
else {
|
||||
"left": definition.text_rectangle.left,
|
||||
"top": definition.text_rectangle.top,
|
||||
"right": definition.text_rectangle.right,
|
||||
"bottom": definition.text_rectangle.bottom,
|
||||
}
|
||||
)
|
||||
text_rectangle_px = None
|
||||
if args.frame is not None and has_text_rectangle:
|
||||
frame_x, frame_y, frame_w, frame_h = args.frame
|
||||
try:
|
||||
evaluated = registry.evaluate(
|
||||
args.preset,
|
||||
frame_w,
|
||||
frame_h,
|
||||
adjustments=_parse_adjustments(args.adjust),
|
||||
)
|
||||
except (ValueError, FormulaEvaluationError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
rect = evaluated.text_rectangle
|
||||
if rect is not None:
|
||||
text_rectangle_px = {
|
||||
"x": round(frame_x + rect.left, 2),
|
||||
"y": round(frame_y + rect.top, 2),
|
||||
"width": round(rect.right - rect.left, 2),
|
||||
"height": round(rect.bottom - rect.top, 2),
|
||||
}
|
||||
semantics = get_preset_shape_semantics().describe(args.preset)
|
||||
full_payload = {
|
||||
"preset": definition.name,
|
||||
@@ -245,6 +293,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
"path_count": path_count,
|
||||
"connection_site_count": connection_site_count,
|
||||
"has_text_rectangle": has_text_rectangle,
|
||||
"text_rectangle": text_rectangle,
|
||||
"text_rectangle_px": text_rectangle_px,
|
||||
"semantics": semantics,
|
||||
}
|
||||
payload = (
|
||||
@@ -260,6 +310,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
"path_count": path_count,
|
||||
"connection_site_count": connection_site_count,
|
||||
"has_text_rectangle": has_text_rectangle,
|
||||
"text_rectangle": text_rectangle,
|
||||
"text_rectangle_px": text_rectangle_px,
|
||||
}
|
||||
if args.compact
|
||||
else full_payload
|
||||
|
||||
+16
-1
@@ -858,8 +858,23 @@ class ProjectManager:
|
||||
continue
|
||||
|
||||
inside_projects = is_within_path(source_path, PROJECTS_ROOT)
|
||||
# A file inside another project's tree (projects/<other>/...) is
|
||||
# that project's material: taking it by default emptied a finished
|
||||
# project's images/ once. Copy unless --move is explicit.
|
||||
inside_other_project = (
|
||||
inside_projects
|
||||
and not is_within_path(source_path, project_dir.resolve())
|
||||
and source_path.resolve().parent != PROJECTS_ROOT.resolve()
|
||||
)
|
||||
if copy:
|
||||
effective_move = False
|
||||
elif inside_other_project and not move:
|
||||
effective_move = False
|
||||
print(
|
||||
f"note: {source_path} belongs to another project; copied "
|
||||
f"(not moved). Pass --move to take it out of that project.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif inside_projects:
|
||||
effective_move = True
|
||||
else:
|
||||
@@ -870,7 +885,7 @@ class ProjectManager:
|
||||
f"(not moved). Only sources under projects/ may be moved.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif inside_projects and not move and not copy:
|
||||
elif inside_projects and not inside_other_project and not move and not copy:
|
||||
print(
|
||||
f"note: {source_path} is under projects/; moved into the target "
|
||||
f"project. Pass --copy to preserve it.",
|
||||
|
||||
@@ -79,6 +79,7 @@ _PARAGRAPH_DEFAULT_FIELDS = (
|
||||
"line_spacing_percent",
|
||||
)
|
||||
_PARAGRAPH_FIELDS = set(_PARAGRAPH_DEFAULT_FIELDS) | {"text", "runs"}
|
||||
_PLAIN_TEXT_RUN_FIELDS = ("bold", "color", "font_size")
|
||||
_RUN_DEFAULT_FIELDS = (
|
||||
"bold",
|
||||
"italic",
|
||||
@@ -234,6 +235,12 @@ def _expand_cell(
|
||||
style = cell_styles[style_name]
|
||||
|
||||
expanded = _format_layers(defaults["cell"], style, cell)
|
||||
if "paragraphs" not in expanded:
|
||||
# A plain-text cell is one run: run defaults reach it unless the
|
||||
# cell (or its cell_style / cell defaults) already set the field.
|
||||
for field in _PLAIN_TEXT_RUN_FIELDS:
|
||||
if field not in expanded and field in defaults["run"]:
|
||||
expanded[field] = copy.deepcopy(defaults["run"][field])
|
||||
if "paragraphs" in expanded:
|
||||
paragraphs = expanded["paragraphs"]
|
||||
if not isinstance(paragraphs, list):
|
||||
|
||||
@@ -584,6 +584,20 @@ def _log_keying_findings(
|
||||
"effect crosses it. Regenerate with a clear key-only gutter; a "
|
||||
"larger --tolerance would eat into the elements."
|
||||
)
|
||||
elif (
|
||||
outlier <= tolerance
|
||||
and any("content reaches the" in finding for finding in findings)
|
||||
and not any("boundary pixels stayed opaque" in finding for finding in findings)
|
||||
):
|
||||
# The key itself passed at this tolerance; only element tips
|
||||
# crossed a gutter. When the farthest gutter pixel exceeds the
|
||||
# tolerance the branch below still offers the rerun instead.
|
||||
_log(
|
||||
" Content crosses a cell edge: no --bg/--tolerance rerun "
|
||||
"can separate it. Regenerate with a wider key-only gutter "
|
||||
"(each element about 65% of its cell, at least 10% key on "
|
||||
"every side, tips and effects included)."
|
||||
)
|
||||
else:
|
||||
pure = _nearest_pure_key(dominant)
|
||||
if pure is not None:
|
||||
|
||||
+3
-1
@@ -190,7 +190,9 @@ def detect_list_item(text: str) -> tuple:
|
||||
if match:
|
||||
return (True, 'ul', marker + ' ' + text[match.end():])
|
||||
|
||||
ol_pattern = r'^(\d+)[.、)]\s*'
|
||||
# ``83.2%`` at the start of a line is a decimal, not item 83: after a
|
||||
# dot the marker must not be followed by another digit.
|
||||
ol_pattern = r'^(\d+)(?:[、)]|\.(?!\d))\s*'
|
||||
match = re.match(ol_pattern, text)
|
||||
if match:
|
||||
num = match.group(1)
|
||||
|
||||
+6
-1
@@ -52,7 +52,7 @@ configure_utf8_stdio()
|
||||
|
||||
try:
|
||||
import requests
|
||||
from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
from bs4 import BeautifulSoup, Comment, NavigableString, Tag
|
||||
except ImportError:
|
||||
print("Error: This script requires 'requests' and 'beautifulsoup4'.")
|
||||
print("Please run: pip install requests beautifulsoup4")
|
||||
@@ -719,6 +719,11 @@ def simple_html_to_markdown_traversal(soup: Tag | BeautifulSoup | None) -> str:
|
||||
lines = []
|
||||
|
||||
def traverse(node: Tag | NavigableString) -> str:
|
||||
if isinstance(node, Comment):
|
||||
# ``Comment`` is a ``NavigableString`` subclass: without this
|
||||
# branch SSR markers such as ``<!--lit-node 1-->`` would be
|
||||
# emitted as body text and even land inside headings.
|
||||
return ""
|
||||
if isinstance(node, NavigableString):
|
||||
text = str(node)
|
||||
# Normalize whitespace but keep single spaces
|
||||
|
||||
@@ -2816,7 +2816,10 @@ class SVGQualityChecker:
|
||||
if element.text:
|
||||
append_run(element, element.text, xml_space)
|
||||
for child in list(element):
|
||||
if not cls._is_tspan(child):
|
||||
# An inline hyperlink (``<a>`` wrapping ``<tspan>`` runs, the
|
||||
# form native-hyperlinks.md requires) is a transparent style
|
||||
# container: its runs are measured like any other inline run.
|
||||
if not (cls._is_tspan(child) or _local_name(child) == 'a'):
|
||||
return False
|
||||
if any(child.get(name) is not None for name in ('x', 'y', 'dx', 'dy')):
|
||||
return False
|
||||
|
||||
+13
-3
@@ -224,7 +224,11 @@ def scan_svg_targets(svg_path: Path) -> tuple[list[GroupTarget], list[str]]:
|
||||
has_explicit_semantics
|
||||
and is_static_page_frame(role, placeholder)
|
||||
)
|
||||
structurally_static = has_structural_layer or semantic_static
|
||||
# A static role/placeholder marker makes a group chrome (kept out of
|
||||
# scaffold, listing, and automatic animation) but not structural: an
|
||||
# explicit sidecar entry may still animate or Morph-pair it. Only a
|
||||
# ``data-pptx-layer`` group is structural.
|
||||
structurally_static = has_structural_layer
|
||||
if has_structural_layer:
|
||||
chrome = True
|
||||
elif has_explicit_semantics:
|
||||
@@ -1693,10 +1697,16 @@ def build_group_listing(project_path: Path) -> tuple[list[str], list[str]]:
|
||||
for slide_name, targets in targets_by_slide.items():
|
||||
_require_unique_target_ids(slide_name, targets)
|
||||
ids = [t.group_id for t in targets if not t.chrome]
|
||||
chrome_ids = [t.group_id for t in targets if t.chrome]
|
||||
if not ids:
|
||||
lines.append(f'{slide_name}: (no animatable groups)')
|
||||
line = f'{slide_name}: (no animatable groups)'
|
||||
else:
|
||||
lines.append(f'{slide_name}: {", ".join(ids)}')
|
||||
line = f'{slide_name}: {", ".join(ids)}'
|
||||
if chrome_ids:
|
||||
# Name what was dropped so an id like ``takeaway-rule`` is seen
|
||||
# as chrome-by-token rather than silently missing.
|
||||
line += f' [chrome, animates only when named: {", ".join(chrome_ids)}]'
|
||||
lines.append(line)
|
||||
return lines, anonymous
|
||||
|
||||
|
||||
|
||||
+17
-16
@@ -1173,16 +1173,14 @@ def convert_g(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
||||
if has_explicit_semantics
|
||||
else is_chrome_id(elem_id)
|
||||
)
|
||||
# Chrome — by name or by static role/placeholder marker — stays out of
|
||||
# automatic animation, but an explicit sidecar entry animates it: a
|
||||
# background field can pan, a cover band can slide. Only a structural
|
||||
# layer (master/layout) is beyond reach, because it is not on the slide.
|
||||
should_animate_group = (
|
||||
ctx.depth == 0
|
||||
and elem_id
|
||||
and (
|
||||
not is_chrome
|
||||
or (
|
||||
not has_explicit_semantics
|
||||
and elem_id in ctx.animation_group_overrides
|
||||
)
|
||||
)
|
||||
and (not is_chrome or elem_id in ctx.animation_group_overrides)
|
||||
and elem.get('data-pptx-layer') is None
|
||||
)
|
||||
visual_children = [
|
||||
@@ -1784,6 +1782,14 @@ def convert_element(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None
|
||||
if adjustments:
|
||||
event['adjustments'] = dict(sorted(adjustments.items()))
|
||||
event.update(metadata)
|
||||
if (
|
||||
ctx.depth == 0
|
||||
and elem_id
|
||||
and elem_id in ctx.animation_group_overrides
|
||||
):
|
||||
# Chrome promotion into layouts/masters must leave an explicitly
|
||||
# animated group on its slide.
|
||||
event['animation_override'] = True
|
||||
ctx.trace_events.append(event)
|
||||
|
||||
if elem.get('data-pptx-part') == 'geometry-detail':
|
||||
@@ -2345,16 +2351,11 @@ def convert_svg_to_slide_shapes(
|
||||
role = child.get('data-pptx-role')
|
||||
placeholder = child.get('data-pptx-placeholder')
|
||||
has_explicit_semantics = role is not None or placeholder is not None
|
||||
structurally_static = (
|
||||
child.get('data-pptx-layer') is not None
|
||||
or (
|
||||
has_explicit_semantics
|
||||
and is_static_page_frame(role, placeholder)
|
||||
)
|
||||
)
|
||||
structurally_static = child.get('data-pptx-layer') is not None
|
||||
legacy_chrome = (
|
||||
not has_explicit_semantics
|
||||
and is_chrome_id(elem_id)
|
||||
is_static_page_frame(role, placeholder)
|
||||
if has_explicit_semantics
|
||||
else is_chrome_id(elem_id)
|
||||
)
|
||||
explicit_legacy_override = (
|
||||
elem_id is not None
|
||||
|
||||
+23
-1
@@ -2126,6 +2126,28 @@ def _is_serif_run(run: dict[str, Any]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Faces whose glyphs run wider than the generic advance table at the same
|
||||
# weight. Measured against the installed fonts on 2026-09-04: Arial Black
|
||||
# renders 24% wider than the caps estimate, Verdana 6%. Only factors ≥ 1 are
|
||||
# listed — a narrower face wastes space, a wider one overflows the bounds.
|
||||
# (mixed-case factor, all-caps factor): the generic table is already
|
||||
# pessimistic on lowercase and optimistic on capitals, so the correction
|
||||
# scales with the run's uppercase fraction like the headroom does.
|
||||
_WIDE_FAMILY_WIDTH_FACTORS = {
|
||||
'arial black': (1.06, 1.25),
|
||||
'verdana': (1.02, 1.08),
|
||||
}
|
||||
|
||||
|
||||
def _family_width_factor(run: dict[str, Any]) -> float:
|
||||
family = str(run.get('font_family') or '').split(',')[0].strip().strip('\'"').lower()
|
||||
factors = _WIDE_FAMILY_WIDTH_FACTORS.get(family)
|
||||
if factors is None:
|
||||
return 1.0
|
||||
base, caps = factors
|
||||
return base + (caps - base) * _uppercase_fraction([run])
|
||||
|
||||
|
||||
def _estimate_run_text_width(run: dict[str, Any]) -> float:
|
||||
"""Estimate one run using the metrics actually emitted to DrawingML."""
|
||||
text = str(run.get('text', ''))
|
||||
@@ -2144,7 +2166,7 @@ def _estimate_run_text_width(run: dict[str, Any]) -> float:
|
||||
)
|
||||
/ FONT_PX_TO_HUNDREDTHS_PT
|
||||
)
|
||||
return sum(cluster_widths) + letter_spacing_px * max(
|
||||
return sum(cluster_widths) * _family_width_factor(run) + letter_spacing_px * max(
|
||||
len(cluster_widths) - 1,
|
||||
0,
|
||||
)
|
||||
|
||||
+5
-1
@@ -566,8 +566,12 @@ def validate_native_object_marker_with_warnings(
|
||||
)
|
||||
if kind in {"chart", "table"} else []
|
||||
)
|
||||
# The final checker reports these as warnings because activation is an
|
||||
# export-time choice; ``--native-charts-and-tables`` refuses the same
|
||||
# findings as errors, so say so where the author reads them.
|
||||
warnings.extend(
|
||||
_projection_warnings_for_validated_marker(
|
||||
f"{warning} (blocks --native-charts-and-tables export)"
|
||||
for warning in _projection_warnings_for_validated_marker(
|
||||
elem,
|
||||
kind,
|
||||
payload,
|
||||
|
||||
+100
-8
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
@@ -156,6 +157,10 @@ def _fallback_chart_colors(
|
||||
"text_color": text_color,
|
||||
"axis_color": _most_common_color(axis_colors) or dominant_stroke,
|
||||
"grid_color": _most_common_color(grid_colors) or dominant_stroke,
|
||||
# Which roles were read from labeled strokes; an unlabeled role fell
|
||||
# back to the marker's dominant stroke and the warning says so.
|
||||
"axis_labeled": bool(axis_colors),
|
||||
"grid_labeled": bool(grid_colors),
|
||||
}
|
||||
|
||||
|
||||
@@ -707,10 +712,20 @@ def _native_chart_style_color_warnings(
|
||||
or payload_color == fallback_color
|
||||
):
|
||||
continue
|
||||
warnings.append(
|
||||
message = (
|
||||
f"Native PPTX chart style.{field_name} #{payload_color} differs "
|
||||
f"from fallback dominant {field_name} #{fallback_color}"
|
||||
f"from fallback dominant {field_name} #{fallback_color}; set "
|
||||
f"style.{field_name} to #{fallback_color} or repaint the fallback "
|
||||
f"so #{payload_color} is its dominant {field_name}"
|
||||
)
|
||||
label_needle = {"axis_color": "axis", "grid_color": "grid"}.get(field_name)
|
||||
if label_needle and not inferred.get(f"{label_needle}_labeled"):
|
||||
message += (
|
||||
f" (no stroke carries an id or class naming '{label_needle}', so "
|
||||
f"the marker's dominant stroke was read instead — label the "
|
||||
f"{label_needle} lines to read their own color)"
|
||||
)
|
||||
warnings.append(message)
|
||||
return warnings
|
||||
|
||||
|
||||
@@ -841,11 +856,65 @@ def _chart_projection_text_variants(value: Any) -> set[str]:
|
||||
return variants
|
||||
|
||||
|
||||
_NUMBER_FORMAT_RE = re.compile(r"^(#,##|#|0)?(0*)(?:\.(0+))?(%?)$")
|
||||
|
||||
|
||||
def _format_number_like_excel(number: float, number_format: str) -> str | None:
|
||||
"""Render ``number`` the way PowerPoint shows a plain Excel format code.
|
||||
|
||||
Covers the codes a chart payload realistically writes — ``0``, ``0.0``,
|
||||
``0.00``, ``#,##0``, ``#,##0.0``, and their ``%`` forms. Anything else
|
||||
returns ``None`` so the caller keeps only the literal variants.
|
||||
"""
|
||||
match = _NUMBER_FORMAT_RE.match(number_format.strip())
|
||||
if match is None:
|
||||
return None
|
||||
grouping, _integers, decimals, percent = match.groups()
|
||||
value = number * 100 if percent else number
|
||||
digits = len(decimals or "")
|
||||
text = f"{value:,.{digits}f}" if grouping == "#,##" else f"{value:.{digits}f}"
|
||||
return f"{text}%" if percent else text
|
||||
|
||||
|
||||
def _chart_number_formats(payload: dict[str, Any]) -> list[str]:
|
||||
"""Collect every ``number_format`` the payload applies to visible numbers."""
|
||||
formats: list[str] = []
|
||||
|
||||
def add(container: Any) -> None:
|
||||
if not isinstance(container, dict):
|
||||
return
|
||||
value = container.get("number_format")
|
||||
if value is None:
|
||||
value = container.get("numberFormat")
|
||||
if isinstance(value, str) and value.strip() and value not in formats:
|
||||
formats.append(value)
|
||||
|
||||
for labels in (payload.get("data_labels"), payload.get("dataLabels")):
|
||||
add(labels)
|
||||
if isinstance(labels, dict):
|
||||
for point in labels.get("points") or []:
|
||||
add(point)
|
||||
for plot in payload.get("plots") or []:
|
||||
if isinstance(plot, dict):
|
||||
add(plot.get("data_labels"))
|
||||
add(plot.get("dataLabels"))
|
||||
for series in payload.get("series") or []:
|
||||
if isinstance(series, dict):
|
||||
add(series.get("data_labels"))
|
||||
add(series.get("dataLabels"))
|
||||
axes = payload.get("axes")
|
||||
if isinstance(axes, dict):
|
||||
for config in axes.values():
|
||||
add(config)
|
||||
return formats
|
||||
|
||||
|
||||
def _chart_projected_texts(
|
||||
payload: dict[str, Any],
|
||||
chart_data: dict[str, Any],
|
||||
) -> set[str]:
|
||||
projected: set[str] = set()
|
||||
number_formats = _chart_number_formats(payload)
|
||||
|
||||
def add(value: Any) -> None:
|
||||
if isinstance(value, dict):
|
||||
@@ -861,6 +930,13 @@ def _chart_projected_texts(
|
||||
add(item)
|
||||
return
|
||||
projected.update(_chart_projection_text_variants(value))
|
||||
numeric = _maybe_number(value)
|
||||
if numeric is None:
|
||||
return
|
||||
for number_format in number_formats:
|
||||
formatted = _format_number_like_excel(numeric, number_format)
|
||||
if formatted:
|
||||
projected.add(formatted)
|
||||
|
||||
for key in ("categories", "levels", "plots", "series", "values"):
|
||||
add(chart_data.get(key))
|
||||
@@ -893,12 +969,14 @@ def _chart_projected_texts(
|
||||
or major_unit <= 0
|
||||
):
|
||||
continue
|
||||
value = minimum
|
||||
for _ in range(1000):
|
||||
# Build each tick from the index, not by accumulating ``major_unit``:
|
||||
# repeated float addition turns the top tick 5.6 into
|
||||
# 5.6000000000000005 and the fallback's "5.6" is then reported missing.
|
||||
for index in range(1000):
|
||||
value = round(minimum + index * major_unit, 10)
|
||||
if value > maximum + major_unit * 1e-6:
|
||||
break
|
||||
projected.update(_chart_projection_text_variants(value))
|
||||
value += major_unit
|
||||
add(value)
|
||||
return projected
|
||||
|
||||
|
||||
@@ -918,10 +996,24 @@ def _native_chart_reverse_text_warnings(
|
||||
return []
|
||||
sample = ", ".join(repr(text) for text in missing[:8])
|
||||
suffix = "" if len(missing) <= 8 else f", and {len(missing) - 8} more"
|
||||
return [
|
||||
message = (
|
||||
"Native PPTX chart visible text not projected: "
|
||||
f"{sample}{suffix}. Use categories/data labels/axis labels/legend or companion text."
|
||||
]
|
||||
)
|
||||
# ``73.0`` in the fallback while the payload value is 73: General format
|
||||
# renders ``73``, so the decimals need an explicit number_format.
|
||||
if any(
|
||||
re.fullmatch(r"-?\d+\.\d+", text)
|
||||
and str(int(float(text))) in projected
|
||||
and float(text).is_integer()
|
||||
for text in missing
|
||||
):
|
||||
message += (
|
||||
" A value that ends in .0 renders without the decimals under the "
|
||||
"General format; set data_labels.number_format (for example "
|
||||
'"0.0") when the fallback shows them.'
|
||||
)
|
||||
return [message]
|
||||
|
||||
|
||||
def _native_chart_chrome_warnings(elem: ET.Element, payload: dict[str, Any]) -> list[str]:
|
||||
|
||||
+76
-17
@@ -1034,6 +1034,44 @@ def _native_table_fill_warnings(
|
||||
return warnings
|
||||
|
||||
|
||||
def _native_table_body_text_warnings(
|
||||
payload: dict[str, Any],
|
||||
table_rows: list[list[Any]],
|
||||
header_rows: int,
|
||||
text_cells: list[tuple[Any, int, int]],
|
||||
) -> list[str]:
|
||||
"""Report fallback body text colours the payload would not carry."""
|
||||
style = payload.get("style") if isinstance(payload.get("style"), dict) else {}
|
||||
style_body_text = _hex_or_none(style.get("body_text"))
|
||||
body_records = [item for item in text_cells if item[1] >= header_rows]
|
||||
if not body_records:
|
||||
return []
|
||||
body_colors = [
|
||||
record.fill
|
||||
for record, _, col_idx in body_records
|
||||
if col_idx != 0 and record.fill
|
||||
]
|
||||
body_color = max(set(body_colors), key=body_colors.count) if body_colors else None
|
||||
missing_colors: set[str] = set()
|
||||
for record, row_idx, col_idx in body_records:
|
||||
if record.fill is None:
|
||||
continue
|
||||
if col_idx == 0 and record.fill != body_color:
|
||||
continue # first-column emphasis is reported separately
|
||||
cell_color = _table_cell_parity_text_style(table_rows[row_idx][col_idx])[1]
|
||||
if cell_color is None:
|
||||
cell_color = style_body_text
|
||||
if cell_color != record.fill:
|
||||
missing_colors.add(record.fill)
|
||||
if not missing_colors:
|
||||
return []
|
||||
return [
|
||||
"Native PPTX table body text color "
|
||||
+ "/".join(f"#{color}" for color in sorted(missing_colors))
|
||||
+ " is not projected to cell color, defaults.run.color, or style.body_text"
|
||||
]
|
||||
|
||||
|
||||
def _native_table_first_column_warnings(
|
||||
table_rows: list[list[Any]],
|
||||
header_rows: int,
|
||||
@@ -1268,6 +1306,14 @@ def _native_table_warnings(
|
||||
text_cells,
|
||||
)
|
||||
)
|
||||
warnings.extend(
|
||||
_native_table_body_text_warnings(
|
||||
payload,
|
||||
table_rows,
|
||||
header_rows,
|
||||
text_cells,
|
||||
)
|
||||
)
|
||||
warnings.extend(
|
||||
_native_table_border_topology_warnings(
|
||||
payload,
|
||||
@@ -1619,11 +1665,18 @@ def _build_native_table(elem: ET.Element, ctx: ConvertContext, payload: dict[str
|
||||
preserve_source_style = native_import_source(elem) == "pptx"
|
||||
|
||||
style = payload.get("style") if isinstance(payload.get("style"), dict) else {}
|
||||
header_fill = _clean_hex(style.get("header_fill"), "#1F4E79")
|
||||
header_text = _clean_hex(style.get("header_text"), "#FFFFFF")
|
||||
body_fill = _clean_hex(style.get("body_fill"), "#FFFFFF")
|
||||
# Fills are only what the payload says: a cell that resolves to no fill
|
||||
# exports as noFill so the slide background shows through, matching a
|
||||
# fallback that draws no cell rect. Text colour keeps a legible default
|
||||
# because an invisible run is worse than a wrong one.
|
||||
header_fill = _hex_or_none(style.get("header_fill"))
|
||||
body_fill = _hex_or_none(style.get("body_fill"))
|
||||
band_fill = _hex_or_none(style.get("band_fill"))
|
||||
body_text = _clean_hex(style.get("body_text"), "#1F2937")
|
||||
band_fill = _clean_hex(style.get("band_fill"), "#F3F6FA")
|
||||
header_text = _clean_hex(
|
||||
style.get("header_text"),
|
||||
"#FFFFFF" if header_fill is not None else body_text,
|
||||
)
|
||||
font_face = str(style["font_family"]) if style.get("font_family") else None
|
||||
body_font_size = _font_size_hpt(style.get("font_size"), 18)
|
||||
band_rows_enabled = _table_bool(
|
||||
@@ -1709,14 +1762,17 @@ def _build_native_table(elem: ET.Element, ctx: ConvertContext, payload: dict[str
|
||||
)
|
||||
align = str(cell_data.get("align") or "l")
|
||||
else:
|
||||
fill = _clean_hex(
|
||||
cell_data.get("fill"),
|
||||
header_fill if is_header else (
|
||||
band_fill
|
||||
if band_rows_enabled and row_idx % 2 == 0 and row_idx
|
||||
else body_fill
|
||||
),
|
||||
)
|
||||
fill = _hex_or_none(cell_data.get("fill"))
|
||||
if fill is None:
|
||||
banded = (
|
||||
band_rows_enabled
|
||||
and band_fill is not None
|
||||
and row_idx % 2 == 0
|
||||
and row_idx
|
||||
)
|
||||
fill = header_fill if is_header else (
|
||||
band_fill if banded else body_fill
|
||||
)
|
||||
color = _clean_hex(
|
||||
cell_data.get("color"),
|
||||
header_text if is_header else body_text,
|
||||
@@ -1845,11 +1901,14 @@ def _build_native_table(elem: ET.Element, ctx: ConvertContext, payload: dict[str
|
||||
style,
|
||||
ctx.theme_color_spec,
|
||||
)
|
||||
fill_xml = _table_fill_xml(
|
||||
fill,
|
||||
_table_fill_opacity(cell_data),
|
||||
ctx.theme_color_spec,
|
||||
)
|
||||
if fill is None and not preserve_source_style:
|
||||
fill_xml = "<a:noFill/>"
|
||||
else:
|
||||
fill_xml = _table_fill_xml(
|
||||
fill,
|
||||
_table_fill_opacity(cell_data),
|
||||
ctx.theme_color_spec,
|
||||
)
|
||||
cells_xml.append(
|
||||
f"<a:tc{merge_attrs}>"
|
||||
"<a:txBody><a:bodyPr/><a:lstStyle/>"
|
||||
|
||||
+25
-2
@@ -524,6 +524,9 @@ def _trace_chrome_shape_ids(
|
||||
for event in trace.get("events", []):
|
||||
if event.get("decision") != "native":
|
||||
continue
|
||||
if event.get("animation_override"):
|
||||
# Explicitly animated chrome stays slide-local.
|
||||
continue
|
||||
semantic_role = event.get("data-pptx-role")
|
||||
placeholder = event.get("data-pptx-placeholder")
|
||||
has_explicit_semantics = (
|
||||
@@ -5547,6 +5550,26 @@ def _build_sequence_targets(
|
||||
svg_id: sid for sid, svg_id in anim_targets
|
||||
}
|
||||
ordered: list[tuple[int, int, int, str, str, dict[str, Any]]] = []
|
||||
# A group without a sidecar ``order`` follows the nearest listed group
|
||||
# before it in SVG order (0 before the first one). Numbering unlisted
|
||||
# groups by raw SVG index collided with explicit orders: a headline at
|
||||
# index 2 landed behind the body block the author had numbered 1 and 2.
|
||||
inherited_orders: list[int] = []
|
||||
current_order = 0
|
||||
for _sid, svg_id in anim_targets:
|
||||
group_value = groups_cfg.get(svg_id, {})
|
||||
explicit = [
|
||||
cfg.get('order')
|
||||
for _path, cfg in (
|
||||
animation_group_effect_entries(group_value, path='')
|
||||
if isinstance(group_value, dict) else []
|
||||
)
|
||||
if isinstance(cfg.get('order'), int)
|
||||
and not isinstance(cfg.get('order'), bool)
|
||||
]
|
||||
if explicit:
|
||||
current_order = max(current_order, max(explicit))
|
||||
inherited_orders.append(current_order)
|
||||
for idx, (sid, svg_id) in enumerate(anim_targets):
|
||||
group_value = groups_cfg.get(svg_id, {})
|
||||
if not isinstance(group_value, dict):
|
||||
@@ -5576,8 +5599,8 @@ def _build_sequence_targets(
|
||||
if animation is None and normalized_effect is None:
|
||||
continue
|
||||
order_value = effect_cfg.get('order')
|
||||
order = order_value if order_value is not None else idx + 1
|
||||
if (
|
||||
order = order_value if order_value is not None else inherited_orders[idx]
|
||||
if order_value is not None and (
|
||||
isinstance(order, bool)
|
||||
or not isinstance(order, int)
|
||||
or order <= 0
|
||||
|
||||
+74
@@ -25,6 +25,7 @@ from svg_to_pptx.native_objects.table import ( # noqa: E402
|
||||
_table_border_xml,
|
||||
_validate_table_payload,
|
||||
)
|
||||
from semantic_table import expand_semantic_table_payload # noqa: E402
|
||||
from svg_to_pptx.pptx_package.cli import ( # noqa: E402
|
||||
_native_object_projection_findings,
|
||||
)
|
||||
@@ -423,5 +424,78 @@ class NativeTablePayloadRoundTripTest(unittest.TestCase):
|
||||
expand_semantic_table_payload(payload) # must still expand cleanly
|
||||
|
||||
|
||||
class NativeTableFillAndTextDefaultsTests(unittest.TestCase):
|
||||
def test_plain_text_cell_inherits_run_defaults(self) -> None:
|
||||
expanded = expand_semantic_table_payload({
|
||||
"schema": "ppt-master.semantic-table.v2",
|
||||
"x": 0, "y": 0, "width": 200, "height": 80,
|
||||
"column_widths": [100, 100],
|
||||
"row_heights": [40, 40],
|
||||
"defaults": {
|
||||
"cell": {"font_size": 20},
|
||||
"run": {"color": "#DAD5CA", "bold": True, "font_size": 16},
|
||||
},
|
||||
"columns": ["A", {"text": "B", "color": "#FFFFFF"}],
|
||||
"rows": [["a", {"text": "b", "bold": False}]],
|
||||
})
|
||||
header_a, header_b = expanded["columns"]
|
||||
body_a, body_b = expanded["rows"][0]
|
||||
self.assertEqual(header_a["color"], "#DAD5CA")
|
||||
self.assertEqual(header_b["color"], "#FFFFFF")
|
||||
self.assertEqual(body_a["color"], "#DAD5CA")
|
||||
self.assertTrue(body_a["bold"])
|
||||
self.assertFalse(body_b["bold"])
|
||||
# defaults.cell wins over defaults.run for a shared field
|
||||
self.assertEqual(body_a["font_size"], 20)
|
||||
|
||||
def test_body_text_color_parity_warns_when_no_layer_carries_it(self) -> None:
|
||||
def marker(rows_json: str, style_json: str = "") -> ET.Element:
|
||||
return ET.fromstring(f"""
|
||||
<g data-pptx-replace-with="table" data-pptx-bounds="0 0 200 80">
|
||||
<metadata type="application/json">
|
||||
{{
|
||||
"schema": "ppt-master.semantic-table.v2",
|
||||
"x": 0, "y": 0, "width": 200, "height": 80,
|
||||
"header_rows": 1,
|
||||
"column_widths": [100, 100],
|
||||
"row_heights": [40, 40],
|
||||
{style_json}
|
||||
"columns": [
|
||||
{{"text": "Level", "color": "#98A0A9"}},
|
||||
{{"text": "Meaning", "color": "#98A0A9"}}
|
||||
],
|
||||
"rows": [{rows_json}]
|
||||
}}
|
||||
</metadata>
|
||||
<line x1="0" y1="0" x2="200" y2="0" stroke="#999999"/>
|
||||
<line x1="0" y1="40" x2="200" y2="40" stroke="#999999"/>
|
||||
<line x1="0" y1="80" x2="200" y2="80" stroke="#999999"/>
|
||||
<g fill="#98A0A9">
|
||||
<text x="10" y="25">Level</text>
|
||||
<text x="110" y="25">Meaning</text>
|
||||
</g>
|
||||
<g fill="#DAD5CA">
|
||||
<text x="10" y="65">Altar</text>
|
||||
<text x="110" y="65">Cosmos</text>
|
||||
</g>
|
||||
</g>
|
||||
""")
|
||||
|
||||
bare = "\n".join(native_object_projection_warnings(
|
||||
marker('["Altar", "Cosmos"]')
|
||||
))
|
||||
self.assertIn("body text color #DAD5CA is not projected", bare)
|
||||
|
||||
via_style = "\n".join(native_object_projection_warnings(
|
||||
marker('["Altar", "Cosmos"]', '"style": {"body_text": "#DAD5CA"},')
|
||||
))
|
||||
self.assertNotIn("body text color", via_style)
|
||||
|
||||
via_run_defaults = "\n".join(native_object_projection_warnings(
|
||||
marker('["Altar", "Cosmos"]', '"defaults": {"run": {"color": "#DAD5CA"}},')
|
||||
))
|
||||
self.assertNotIn("body text color", via_run_defaults)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -43,6 +43,8 @@ _LATIN_TOKEN_CONNECTORS = frozenset("'’._:/+%@#-")
|
||||
_WEIGHTS = ('normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900')
|
||||
_CALIBRATION_CJK_SAMPLE = '天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往'
|
||||
_CALIBRATION_LATIN_SAMPLE = 'Clear Slides Make Big Ideas Easy to See.'
|
||||
_CALIBRATION_CAPS_SAMPLE = 'CLEAR SLIDES MAKE BIG IDEAS EASY TO SEE.'
|
||||
_CALIBRATION_DIGITS_SAMPLE = '0123456789'
|
||||
_CORE_CALIBRATION_ROLES = ('body', 'title', 'subtitle', 'annotation')
|
||||
_SLIDE_HEADING_RE = re.compile(
|
||||
r'^#{3,6}[ \t]+Slide[ \t]+([0-9]+|NN)\b.*$',
|
||||
@@ -473,15 +475,20 @@ def _calibration_payload(
|
||||
)
|
||||
cjk_length = len(split_project_text_clusters(_CALIBRATION_CJK_SAMPLE))
|
||||
latin_length = len(split_project_text_clusters(_CALIBRATION_LATIN_SAMPLE))
|
||||
digits_length = len(split_project_text_clusters(_CALIBRATION_DIGITS_SAMPLE))
|
||||
role_rows = {}
|
||||
for name, family, size in roles:
|
||||
cjk_width = measure_text(_CALIBRATION_CJK_SAMPLE, size=size, family=family)
|
||||
latin_width = measure_text(_CALIBRATION_LATIN_SAMPLE, size=size, family=family)
|
||||
caps_width = measure_text(_CALIBRATION_CAPS_SAMPLE, size=size, family=family)
|
||||
digits_width = measure_text(_CALIBRATION_DIGITS_SAMPLE, size=size, family=family)
|
||||
role_rows[name] = {
|
||||
'family': family,
|
||||
'size': size,
|
||||
'cjk_chars_per_100px': round(100.0 * cjk_length / cjk_width, 1),
|
||||
'latin_chars_per_100px': round(100.0 * latin_length / latin_width, 1),
|
||||
'caps_chars_per_100px': round(100.0 * latin_length / caps_width, 1),
|
||||
'digits_chars_per_100px': round(100.0 * digits_length / digits_width, 1),
|
||||
'longest_planned_line': longest[name],
|
||||
}
|
||||
return {
|
||||
@@ -516,7 +523,7 @@ def _fallback_notes(
|
||||
def _render_calibration_table(payload: dict[str, object], *, include_outline: bool) -> str:
|
||||
role_rows = payload['roles']
|
||||
assert isinstance(role_rows, dict)
|
||||
headers = ['role', 'family', 'size', 'CJK ≈chars/100px', 'Latin ≈chars/100px']
|
||||
headers = ['role', 'family', 'size', 'CJK ≈chars/100px', 'Latin ≈chars/100px', 'CAPS ≈chars/100px', 'DIGITS ≈chars/100px']
|
||||
if include_outline:
|
||||
headers.append('longest planned line (px, slide, text)')
|
||||
lines = [
|
||||
@@ -532,6 +539,8 @@ def _render_calibration_table(payload: dict[str, object], *, include_outline: bo
|
||||
_format_number(float(raw_row['size'])),
|
||||
f'{raw_row["cjk_chars_per_100px"]:.1f}',
|
||||
f'{raw_row["latin_chars_per_100px"]:.1f}',
|
||||
f'{raw_row["caps_chars_per_100px"]:.1f}',
|
||||
f'{raw_row["digits_chars_per_100px"]:.1f}',
|
||||
]
|
||||
if include_outline:
|
||||
planned = raw_row['longest_planned_line']
|
||||
@@ -545,7 +554,22 @@ def _render_calibration_table(payload: dict[str, object], *, include_outline: bo
|
||||
lines.append(f'[NOTE] {note}')
|
||||
lines.append(
|
||||
'[NOTE] mixed line width ≈ (CJK chars ÷ CJK rate + other chars ÷ Latin '
|
||||
'rate) × 100; spaces, digits, and punctuation count as Latin.'
|
||||
'rate) × 100; spaces and punctuation count as Latin, digits use the '
|
||||
'DIGITS rate.'
|
||||
)
|
||||
if include_outline:
|
||||
lines.append(
|
||||
'[NOTE] the longest planned line is the §IX wording; a line rewritten '
|
||||
'while authoring (an expanded title, a longer label) is re-estimated '
|
||||
'with the rates — the outline column does not cover it.'
|
||||
)
|
||||
lines.append(
|
||||
'[NOTE] rates are sample averages measured with the checker\'s '
|
||||
'estimator (headroom included); the checker measures each real line '
|
||||
'glyph by glyph, so capital-heavy words (WebGPU, GDP), digits (1935, '
|
||||
'83.2%) and wide letters run wider than the Latin rate — use the CAPS '
|
||||
'rate for acronyms and uppercase, the DIGITS rate for numbers, and keep '
|
||||
'about 5% below any bounds width.'
|
||||
)
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
@@ -588,6 +612,19 @@ def _run_calibrate(args: argparse.Namespace) -> int:
|
||||
payload['notes'] = _fallback_notes(roles, fallbacks)
|
||||
output_path = project_path / 'validation' / 'text_calibration.json'
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if args.role and output_path.is_file():
|
||||
# A --role run recalibrates only the named roles; keep the roles
|
||||
# an earlier run already wrote, so "calibrate again only for a
|
||||
# role never calibrated" is incremental, not a table overwrite.
|
||||
try:
|
||||
previous = json.loads(output_path.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
previous = {}
|
||||
previous_roles = previous.get('roles') if isinstance(previous, dict) else None
|
||||
if isinstance(previous_roles, dict):
|
||||
merged = dict(previous_roles)
|
||||
merged.update(payload['roles'])
|
||||
payload['roles'] = merged
|
||||
rendered_json = json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
output_path.write_text(rendered_json + '\n', encoding='utf-8')
|
||||
except (OSError, ValueError) as exc:
|
||||
|
||||
@@ -56,7 +56,7 @@ The dispatcher writes standard Markdown plus a conversion profile beside each so
|
||||
|
||||
```bash
|
||||
python3 ${SKILL_DIR}/scripts/project_manager.py init <project_name>
|
||||
python3 ${SKILL_DIR}/scripts/project_manager.py import-sources <project_path> <source_files_or_dirs...> # skip when content is only in conversation
|
||||
python3 ${SKILL_DIR}/scripts/project_manager.py import-sources <project_path> <source_files_or_dirs...> # skip when content is only in conversation; another project's files are copied unless --move
|
||||
```
|
||||
|
||||
**Hard rule — truthful canvas token**: append `--format <registered_format>` only when an explicit user/source fact already establishes an exact registered canvas ([`canvas-formats.md`](../references/canvas-formats.md)). Otherwise Stage 1 confirms the canvas, starting from the project-initialization canvas unless the user/source context changes it, and `spec_lock.md` records its viewBox.
|
||||
@@ -287,7 +287,7 @@ gate-signal: method=<rule resolved, or none> | page-local=<count> | not-exercise
|
||||
|
||||
| Signal | Reading |
|
||||
|---|---|
|
||||
| Two or more issues share a category and direction — on one page or across pages | Method-level bias — resolve to the authoritative rule before P06 (for text extents: correct the per-role calibration table — rerun `python3 ${SKILL_DIR}/scripts/text_measure.py calibrate <project_path> --outline` or add the missing `--role` — then every later page estimates by that arithmetic and measures nothing); a correction fitted to the observed offset patches only this sample |
|
||||
| Two or more issues share a category and direction — on one page or across pages — or one issue sits in a writing form the deck repeats on every page (a paragraph construction, a header line, a card frame), which a long deck copies before the final gate | Method-level bias — resolve to the authoritative rule before P06 (for text extents: correct the per-role calibration table — rerun `python3 ${SKILL_DIR}/scripts/text_measure.py calibrate <project_path> --outline` or add the missing `--role` — then every later page estimates by that arithmetic and measures nothing); a correction fitted to the observed offset patches only this sample |
|
||||
| One isolated issue tied to a single page's structure | Page-local — fix and continue |
|
||||
| A recurring element (furniture, caption format, section numbering, accent discipline) drifts between its occurrences or is still unsettled | It will be copied to every later page — settle its semantics now |
|
||||
|
||||
|
||||
+2
@@ -80,6 +80,8 @@ python3 ${SKILL_DIR}/scripts/project_manager.py import-sources \
|
||||
|
||||
**Project state**: `init` creates `svg_output/` and the cold `validation/workflow.log` (auto-recorded by later tools; one manual note only for a material handoff, rework reason, approved exception, or manual recovery; never read during a run and never a resume source). Use a new path or one whose `svg_output/` is empty. Quick ignores any existing Design Spec or lock and never scaffolds one.
|
||||
|
||||
A file taken from another project's tree is copied unless `--move` is explicit; a loose file under `projects/` is moved unless `--copy` is passed.
|
||||
|
||||
**✅ Checkpoint — every named input landed**: `import-sources` exits 0 when one input succeeds; read the printed `skipped` reasons. "equivalent content exists" is benign; `path not found`, failed conversion, or no usable Markdown means the source is absent: re-import, supply a converted equivalent, or state why the deck proceeds without it. Pass a source once when Markdown sits beside it, both locations when `-o` wrote elsewhere. Copy/move semantics, bitmap archiving, EMF/WMF handling (never PNG), and the PPTX intake bundle under `analysis/` are [`project.md`](../../scripts/docs/project.md) — source facts, not replica constraints.
|
||||
|
||||
**URL authority**: the facts JSON is the sole URL authority; only after web-image search is exhausted may a webpage package be fetched under [`topic-research`](../stages/topic-research.md) § Hand-off and its accepted images copied in. Under Image to PPTX, the normalized frame roster is canonical input and the agent writes `analysis/reconstruction_inventory.json` before deciding layers.
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ Unless an all-motion disable bypasses it, validate an existing sidecar first: `p
|
||||
|
||||
**Forbidden — group-list-first choreography**: choosing effects or order from `list-groups` before the audit; keeping a coarse wrapper because it has an `id`; splitting one idea into shapes or lines to raise the count; merging unrelated ideas to lower it; adding animation `data-*` attributes to SVG. There is no target group count.
|
||||
|
||||
After any regrouping, rerun the final gate (`svg_quality_checker.py <project_path> --canonical-authoring --stage final --json`; Quick inserts `--quick-generate` before `--stage`), then list the post-regroup anchors with `animation_config.py list-groups <project_path>` (one line per slide, chrome groups `bg` / `*-header` / `*-footer` / `*-decor` / `nav` / `watermark` / `logo` / `pagenumber` excluded; a `*-header` or `*-footer` group that holds the page's largest text is the title block, not chrome, and stays listed). That list is the only source of slide and group keys for §3–§4. An explicit sidecar entry overrides only the marker-free legacy id-name heuristic; a group carrying `data-pptx-layer` or a static role/placeholder marker never animates. If a starting file is useful, `animation_config.py scaffold <project_path>` after regrouping creates a neutral scaffold (default object effect `none`, groups as empty `{}` placeholders) — creating it selects nothing, and it need not be read in full.
|
||||
After any regrouping, rerun the final gate (`svg_quality_checker.py <project_path> --canonical-authoring --stage final --json`; Quick inserts `--quick-generate` before `--stage`), then list the post-regroup anchors with `animation_config.py list-groups <project_path>` (one line per slide, chrome groups excluded and named at the end of the line — an id equal to, or holding a `-`/`_` token from, `bg` / `background` / `header` / `footer` / `decor` / `decoration(s)` / `chrome` / `nav` / `watermark` / `logo` / `pagenumber` / `pagenum` / `slidenumber` / `slidenum` / `rule`, so `takeaway-rule` is chrome by name; a `*-header` or `*-footer` group that holds the page's largest text is the title block, not chrome, and stays listed). That list is the only source of slide and group keys for §3–§4. An explicit sidecar entry overrides only the marker-free legacy id-name heuristic; a `data-pptx-layer` group never animates; a static role/placeholder group animates only when a sidecar entry names it. If a starting file is useful, `animation_config.py scaffold <project_path>` after regrouping creates a neutral scaffold (default object effect `none`, groups as empty `{}` placeholders) — creating it selects nothing, and it need not be read in full.
|
||||
|
||||
---
|
||||
|
||||
@@ -77,7 +77,7 @@ After any regrouping, rerun the final gate (`svg_quality_checker.py <project_pat
|
||||
|
||||
**Mandatory**: plan the requested layers for each affected slide before editing — page transition (`defaults.transition` / `slides.<slide>.transition`), deterministic Morph pair (`slides.<destination>.morph`), page animation defaults (`defaults.animation` / `slides.<slide>.animation`), object lifecycle (`slides.<slide>.groups.<group_id>` as one legacy row or an ordered `effects[]`). A local object request needs no deck-wide transition review.
|
||||
|
||||
**Per-affected-page motion brief**: classify the communication job (including none) and each unit's lifecycle; choose only the required transition, effect, order, timing, and one dominant Start rhythm; mix modes or add emphasis/exit only for a distinct job with a restrained effect. **Mandatory — select from meaning, not catalog coverage**: run the page-relationship and lifecycle playbooks in `animations.md` §3–§4 before any specific effect; candidates are recall aids. **Title motion**: classify the lifecycle, then choose immediate, delayed, synchronized, post-hero, or narration-cued timing; use the sidecar override for a marker-free chrome-like id and repair an incorrect structural marker before animating it. **Default — inherit unaffected layers (may override when the page's job requires it)**: leave the transition and untouched pages on defaults; add a slide-specific `transition` only when the page needs one. **Timing**: shorter for dense/repeated scan content, longer for pivots, hero diagrams, section boundaries, and takeaways; uniform timing is valid. **Reference — motion judgment**: decide job, lifecycle, tone, audience order, and whether direction carries meaning before geometry; a unit that gains no clarity or feeling is `static` (`none`, `entrance_appear`, or `entrance_fade` only when that matches the lifecycle); layout direction alone requires no motion; variation follows content or tone, never quota.
|
||||
**Per-affected-page motion brief**: classify the communication job (including none) and each unit's lifecycle — the page's lead carrier (the showcase image, the hero figure, the chart) enters with intent, framing elements (lettering, slug, numeral) behave the same way on every page of the deck, and supporting pieces (small doodles, arrows, ornaments) stay still; choose only the required transition, effect, order, timing, and one dominant Start rhythm; order follows the reading path — the slug, numeral, or headline that frames a page enters before the units it frames, and an unlisted group takes the position of the nearest listed group above it ([`pptx-animations.md`](../../scripts/docs/pptx-animations.md) §8), so number only what departs from that; mix modes or add emphasis/exit only for a distinct job with a restrained effect. **Mandatory — select from meaning, not catalog coverage**: run the page-relationship and lifecycle playbooks in `animations.md` §3–§4 before any specific effect; candidates are recall aids. **Title motion**: classify the lifecycle, then choose immediate, delayed, synchronized, post-hero, or narration-cued timing; use the sidecar override for a marker-free chrome-like id and repair an incorrect structural marker before animating it. **Default — inherit unaffected layers (may override when the page's job requires it)**: leave the transition and untouched pages on defaults; add a slide-specific `transition` only when the page needs one. **Timing**: shorter for dense/repeated scan content, longer for pivots, hero diagrams, section boundaries, and takeaways; uniform timing is valid. **Reference — motion judgment**: decide job, lifecycle, tone, audience order, and whether direction carries meaning before geometry; a unit that gains no clarity or feeling is `static` (`none`, `entrance_appear`, or `entrance_fade` only when that matches the lifecycle); layout direction alone requires no motion; variation follows content or tone, never quota.
|
||||
|
||||
### 3.1 Supported Page Transitions
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "shadcn",
|
||||
"repo": "https://github.com/shadcn-ui/ui.git",
|
||||
"ref": "main",
|
||||
"commit": "71e50952fbb7eda2c992660d36cd58671a2edf42",
|
||||
"commit": "7c9eaba1c0a6404c990c144a654792e3313c650d",
|
||||
"adapter": "claude-skill",
|
||||
"sourcePath": "skills/shadcn",
|
||||
"syncedAt": "2026-09-03T02:28:39Z"
|
||||
"syncedAt": "2026-09-04T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ Use the `cn()` utility from the project for conditional or merged class names. D
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "cn"
|
||||
|
||||
<div className={cn("flex items-center", isActive ? "bg-primary text-primary-foreground" : "bg-muted")}>
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user