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:
@@ -220,30 +220,6 @@
|
|||||||
},
|
},
|
||||||
"category": "开发工具"
|
"category": "开发工具"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ui-ux-pro-max",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/codex/plugins/ui-ux-pro-max"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "设计"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shadcn",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/codex/plugins/shadcn"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "开发工具"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "frontend-slides",
|
"name": "frontend-slides",
|
||||||
"source": {
|
"source": {
|
||||||
@@ -280,6 +256,42 @@
|
|||||||
},
|
},
|
||||||
"category": "文档处理"
|
"category": "文档处理"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "design-md",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/codex/plugins/design-md"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "知识库与检索"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ui-ux-pro-max",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/codex/plugins/ui-ux-pro-max"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "设计"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shadcn",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/codex/plugins/shadcn"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "开发工具"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ppt-master",
|
"name": "ppt-master",
|
||||||
"source": {
|
"source": {
|
||||||
@@ -315,18 +327,6 @@
|
|||||||
"authentication": "ON_INSTALL"
|
"authentication": "ON_INSTALL"
|
||||||
},
|
},
|
||||||
"category": "MCP"
|
"category": "MCP"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "design-md",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/codex/plugins/design-md"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "知识库与检索"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "a13b2a02fd58d8ad325fefd9d146f62708be8456",
|
"commit": "bdf1179bcf641cca49ee7a5f76df14c3015fd38c",
|
||||||
"syncedAt": "2026-06-24T03:56:08Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "shadcn",
|
"id": "shadcn",
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
"repo": "https://github.com/shadcn-ui/ui.git",
|
"repo": "https://github.com/shadcn-ui/ui.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "b59f68ecc5216e39d3f492e79b9767388ccd43db",
|
"commit": "549852bffcad7718b92bf40ea44b94a4e627511a",
|
||||||
"syncedAt": "2026-06-24T03:56:08Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "frontend-slides",
|
"id": "frontend-slides",
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "54ef7c733480ca44061fc76d6d29dcf2227ecd67",
|
"commit": "7647212fabf12d16c2e90525d0f3f6229ec058a3",
|
||||||
"syncedAt": "2026-06-24T03:56:08Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "next-skills",
|
"id": "next-skills",
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
"repo": "https://github.com/vercel/next.js.git",
|
"repo": "https://github.com/vercel/next.js.git",
|
||||||
"ref": "canary",
|
"ref": "canary",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"commit": "16db9d05442fc1521c02e6690599aa89da0df0ec",
|
"commit": "72c26d1442602c9ab000354b7addb63841884a91",
|
||||||
"syncedAt": "2026-06-24T03:48:19Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,30 +220,6 @@
|
|||||||
},
|
},
|
||||||
"category": "开发工具"
|
"category": "开发工具"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ui-ux-pro-max",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/ui-ux-pro-max"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "设计"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shadcn",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/shadcn"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "开发工具"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "frontend-slides",
|
"name": "frontend-slides",
|
||||||
"source": {
|
"source": {
|
||||||
@@ -280,6 +256,42 @@
|
|||||||
},
|
},
|
||||||
"category": "文档处理"
|
"category": "文档处理"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "design-md",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/design-md"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "知识库与检索"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ui-ux-pro-max",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/ui-ux-pro-max"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "设计"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shadcn",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/shadcn"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "开发工具"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ppt-master",
|
"name": "ppt-master",
|
||||||
"source": {
|
"source": {
|
||||||
@@ -315,18 +327,6 @@
|
|||||||
"authentication": "ON_INSTALL"
|
"authentication": "ON_INSTALL"
|
||||||
},
|
},
|
||||||
"category": "MCP"
|
"category": "MCP"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "design-md",
|
|
||||||
"source": {
|
|
||||||
"source": "local",
|
|
||||||
"path": "./plugins/design-md"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE",
|
|
||||||
"authentication": "ON_INSTALL"
|
|
||||||
},
|
|
||||||
"category": "知识库与检索"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"name": "playwright浏览器自动化操作",
|
"name": "playwright浏览器自动化操作",
|
||||||
"version": "20260605",
|
"version": "20260605",
|
||||||
"keySource": "none",
|
"keySource": "none",
|
||||||
"syncedAt": "2026-06-24T03:57:12Z"
|
"syncedAt": "2026-06-24T16:01:19Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "next-skills",
|
"sourceId": "next-skills",
|
||||||
"repo": "https://github.com/vercel/next.js.git",
|
"repo": "https://github.com/vercel/next.js.git",
|
||||||
"ref": "canary",
|
"ref": "canary",
|
||||||
"commit": "16db9d05442fc1521c02e6690599aa89da0df0ec",
|
"commit": "72c26d1442602c9ab000354b7addb63841884a91",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"sourcePath": "skills",
|
"sourcePath": "skills",
|
||||||
"syncedAt": "2026-06-24T03:48:19Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Confirm each item before starting milestone A. The skill won't apply cleanly if
|
|||||||
- `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
|
- `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
|
||||||
- Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.
|
- Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.
|
||||||
|
|
||||||
- **No incompatible config keys.** `cacheComponents: true` errors on any page that still exports `dynamic`. Other legacy route segment configs (`revalidate`, `fetchCache`) and the renamed `experimental.dynamicIO` / `experimental.useCache` should also be migrated. Resolve them via the [migration guide's "Enable Cache Components" section](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components) before starting.
|
- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. That covers pages and layouts, route handlers (`route.ts`), and metadata routes (`opengraph-image`, `icon`, `apple-icon`, `sitemap`, `manifest`, `robots`). The renamed `experimental.dynamicIO` / `experimental.useCache` should also be migrated. Resolve them via the [migration guide's "Enable Cache Components" section](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components) before starting.
|
||||||
|
|
||||||
### notes
|
### notes
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ Ask the user; don't assume. **In a non-interactive run** (no way to prompt), def
|
|||||||
npx @next/codemod@canary cache-components-instant-false ./app
|
npx @next/codemod@canary cache-components-instant-false ./app
|
||||||
```
|
```
|
||||||
|
|
||||||
If `@next/codemod@latest` reports `Invalid transform choice`, try `@canary` — new transforms land there first.
|
If `@next/codemod@latest` reports `Invalid transform choice`, try `@canary` — new transforms land there first. The codemod refuses to run on a dirty working tree; commit or stash unrelated work first, or pass `--force` to override (the codemod's own edits then land alongside your WIP).
|
||||||
|
|
||||||
Inserts `export const instant = false` (with a `// TODO: Cache Components adoption` comment) into every `app/**/{page,layout,default}` file, skipping files that already declare `instant` and any module marked `"use client"` or `"use server"`. Then set `cacheComponents: true`. The TODO comments are the work queue for milestone B.
|
Inserts `export const instant = false` (with a `// TODO: Cache Components adoption` comment) into every `app/**/{page,layout,default}` file, skipping files that already declare `instant` and any module marked `"use client"` or `"use server"`. Then set `cacheComponents: true`. The TODO comments are the work queue for milestone B.
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
English | [中文](./README_CN.md)
|
English | [中文](./README_CN.md)
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary>This project is kept free and open source with the support of <a href="https://www.packyapi.com/register?aff=ppt-master">PackyCode</a>, <a href="https://apikey.fun/register?aff=PPT-MASTER">APIKEY.FUN</a>, <a href="https://runapi.co/register?aff=WMLJ">RunAPI</a> and other sponsors.</summary>
|
<summary>This project is kept free and open source with the support of <a href="https://www.packyapi.com/register?aff=ppt-master">PackyCode</a>, <a href="https://apikey.fun/register?aff=PPT-MASTER">APIKEY.FUN</a>, <a href="https://runapi.co/register?aff=WMLJ">RunAPI</a>, <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624">YouYun ZhiSuan</a> and other sponsors.</summary>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -28,6 +28,10 @@ English | [中文](./README_CN.md)
|
|||||||
<td width="180"><a href="https://runapi.co/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a></td>
|
<td width="180"><a href="https://runapi.co/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a></td>
|
||||||
<td>Thanks to RunAPI for sponsoring this project! RunAPI is an efficient and stable API platform — a single API Key gives you access to 150+ leading models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, at prices as low as <strong>10% of official rates</strong>, with exceptional stability and seamless compatibility with tools like Claude Code. RunAPI offers an exclusive perk for PPT Master users: register and contact an administrator via <a href="https://runapi.co/register?aff=WMLJ">our dedicated link</a> to claim <strong>¥7 in free credit</strong>.</td>
|
<td>Thanks to RunAPI for sponsoring this project! RunAPI is an efficient and stable API platform — a single API Key gives you access to 150+ leading models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, at prices as low as <strong>10% of official rates</strong>, with exceptional stability and seamless compatibility with tools like Claude Code. RunAPI offers an exclusive perk for PPT Master users: register and contact an administrator via <a href="https://runapi.co/register?aff=WMLJ">our dedicated link</a> to claim <strong>¥7 in free credit</strong>.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="docs/assets/sponsors/youyun.png" alt="YouYun ZhiSuan" width="150"></a></td>
|
||||||
|
<td>Thanks to YouYun ZhiSuan for sponsoring this project! YouYun ZhiSuan is UCloud's AI cloud platform, providing one-stop API services for mainstream domestic and international models, all accessible with a single key. The platform features cost-effective CodingPlan packages for domestic models (including GLM5.2, Deepseek-v4, and more), along with official channels for stable access to overseas models, meeting diverse development needs. It's compatible with mainstream AI coding tools like Claude Code and Codex, as well as general API calls. The platform supports enterprise-level high concurrency, 24/7 technical support, and self-service invoicing. Register through <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624">this link</a> to receive up to <strong>¥10 in free credits</strong> and get started right away.</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -354,6 +358,8 @@ PPT Master is currently built and maintained primarily by me. Every new template
|
|||||||
|
|
||||||
<a href="https://runapi.co/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" height="40" /></a>
|
<a href="https://runapi.co/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" height="40" /></a>
|
||||||
|
|
||||||
|
<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="docs/assets/sponsors/youyun.png" alt="YouYun ZhiSuan" height="40" /></a>
|
||||||
|
|
||||||
<a href="https://m.do.co/c/547f129aabe1"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" alt="Powered by DigitalOcean" height="40" /></a>
|
<a href="https://m.do.co/c/547f129aabe1"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" alt="Powered by DigitalOcean" height="40" /></a>
|
||||||
|
|
||||||
**Individual support**
|
**Individual support**
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "ppt-master",
|
"sourceId": "ppt-master",
|
||||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "54ef7c733480ca44061fc76d6d29dcf2227ecd67",
|
"commit": "7647212fabf12d16c2e90525d0f3f6229ec058a3",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": "skills/ppt-master",
|
"sourcePath": "skills/ppt-master",
|
||||||
"syncedAt": "2026-06-24T03:56:08Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-9
@@ -831,7 +831,15 @@ def _char_width(ch: str, font_size: float, bold: bool) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def _estimate_run_width(text: str, run: TextRun) -> float:
|
def _estimate_run_width(text: str, run: TextRun) -> float:
|
||||||
return sum(_char_width(c, run.font_size_px, run.bold) for c in text) * 1.05
|
glyph_width = sum(_char_width(c, run.font_size_px, run.bold) for c in text)
|
||||||
|
tracking_width = run.letter_spacing_px * max(len(text) - 1, 0)
|
||||||
|
return (glyph_width + tracking_width) * 1.05
|
||||||
|
|
||||||
|
|
||||||
|
def _advance_width(ch: str, index_in_segment: int, run: TextRun) -> float:
|
||||||
|
"""Return the width added by one character inside a measured line segment."""
|
||||||
|
tracking = run.letter_spacing_px if index_in_segment > 0 else 0.0
|
||||||
|
return _char_width(ch, run.font_size_px, run.bold) + tracking
|
||||||
|
|
||||||
|
|
||||||
def _find_break_point(
|
def _find_break_point(
|
||||||
@@ -849,7 +857,7 @@ def _find_break_point(
|
|||||||
|
|
||||||
for i in range(start, len(text)):
|
for i in range(start, len(text)):
|
||||||
ch = text[i]
|
ch = text[i]
|
||||||
ch_w = _char_width(ch, run.font_size_px, run.bold)
|
ch_w = _advance_width(ch, i - start, run)
|
||||||
if cur_w + ch_w > max_width:
|
if cur_w + ch_w > max_width:
|
||||||
if last_break > start:
|
if last_break > start:
|
||||||
return last_break, last_break_w
|
return last_break, last_break_w
|
||||||
@@ -898,7 +906,6 @@ def _wrap_paragraph_into_lines(
|
|||||||
text = run.text
|
text = run.text
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(text):
|
while i < len(text):
|
||||||
remaining = text[i:]
|
|
||||||
avail = max_width - cur_w
|
avail = max_width - cur_w
|
||||||
if avail <= 0 and lines[-1]:
|
if avail <= 0 and lines[-1]:
|
||||||
# Line is full; start a new one
|
# Line is full; start a new one
|
||||||
@@ -906,21 +913,21 @@ def _wrap_paragraph_into_lines(
|
|||||||
cur_w = 0.0
|
cur_w = 0.0
|
||||||
avail = max_width
|
avail = max_width
|
||||||
|
|
||||||
end, used = _find_break_point(remaining, 0, avail, run)
|
end, used = _find_break_point(text, i, avail, run)
|
||||||
if end == 0:
|
if end == i:
|
||||||
# Nothing fits even from a fresh line — force one char to avoid
|
# Nothing fits even from a fresh line — force one char to avoid
|
||||||
# an infinite loop.
|
# an infinite loop.
|
||||||
if lines[-1]:
|
if lines[-1]:
|
||||||
lines.append([])
|
lines.append([])
|
||||||
cur_w = 0.0
|
cur_w = 0.0
|
||||||
continue
|
continue
|
||||||
end = 1
|
end = i + 1
|
||||||
used = _char_width(remaining[0], run.font_size_px, run.bold)
|
used = _advance_width(text[i], 0, run)
|
||||||
|
|
||||||
chunk = remaining[:end]
|
chunk = text[i:end]
|
||||||
lines[-1].append(_copy_run(run, text=chunk))
|
lines[-1].append(_copy_run(run, text=chunk))
|
||||||
cur_w += used
|
cur_w += used
|
||||||
i += end
|
i = end
|
||||||
|
|
||||||
if i < len(text):
|
if i < len(text):
|
||||||
# More to render — wrap to next line
|
# More to render — wrap to next line
|
||||||
|
|||||||
+2
-1
@@ -205,7 +205,8 @@ def embed_images_in_svg(svg_path: str, dry_run: bool = False,
|
|||||||
with open(svg_path, 'w', encoding='utf-8') as f:
|
with open(svg_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
|
|
||||||
return (images_embedded, new_size)
|
processed_count = len(images_found) if dry_run else images_embedded
|
||||||
|
return (processed_count, new_size)
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Run the CLI entry point."""
|
"""Run the CLI entry point."""
|
||||||
|
|||||||
+104
-19
@@ -821,6 +821,23 @@ def convert_polyline(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | Non
|
|||||||
# text
|
# text
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_SERIF_WIDTH_FAMILIES = {
|
||||||
|
'book antiqua',
|
||||||
|
'cambria',
|
||||||
|
'fangsong',
|
||||||
|
'garamond',
|
||||||
|
'georgia',
|
||||||
|
'kaiti',
|
||||||
|
'palatino',
|
||||||
|
'palatino linotype',
|
||||||
|
'serif',
|
||||||
|
'simsun',
|
||||||
|
'songti',
|
||||||
|
'times',
|
||||||
|
'times new roman',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_text(text: str, *, preserve_space: bool = False) -> str:
|
def _normalize_text(text: str, *, preserve_space: bool = False) -> str:
|
||||||
"""Collapse runs of whitespace into a single space; do NOT strip the ends.
|
"""Collapse runs of whitespace into a single space; do NOT strip the ends.
|
||||||
|
|
||||||
@@ -845,6 +862,73 @@ def _preserves_space(elem: ET.Element) -> bool:
|
|||||||
return xml_space == 'preserve'
|
return xml_space == 'preserve'
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_letter_spacing_px(
|
||||||
|
value: str | None,
|
||||||
|
*,
|
||||||
|
font_size: float,
|
||||||
|
scale_x: float = 1.0,
|
||||||
|
) -> float:
|
||||||
|
"""Parse an SVG letter-spacing value into scaled pixels."""
|
||||||
|
if not value:
|
||||||
|
return 0.0
|
||||||
|
raw = value.strip().lower()
|
||||||
|
if raw in {'normal', 'inherit', 'initial', 'unset'}:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
match = re.fullmatch(r'([-+]?(?:\d*\.\d+|\d+\.?))(px|pt|em)?', raw)
|
||||||
|
if not match:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
amount = float(match.group(1))
|
||||||
|
unit = match.group(2) or 'px'
|
||||||
|
if unit == 'em':
|
||||||
|
return amount * font_size
|
||||||
|
if unit == 'pt':
|
||||||
|
return amount * 4.0 / 3.0 * scale_x
|
||||||
|
return amount * scale_x
|
||||||
|
|
||||||
|
|
||||||
|
def _letter_spacing_to_drawingml_spc(letter_spacing_px: float) -> str:
|
||||||
|
"""Convert SVG px letter spacing into DrawingML rPr@spc."""
|
||||||
|
if abs(letter_spacing_px) < 1e-9:
|
||||||
|
return ''
|
||||||
|
spc_val = round(letter_spacing_px * FONT_PX_TO_HUNDREDTHS_PT)
|
||||||
|
return f' spc="{spc_val}"'
|
||||||
|
|
||||||
|
|
||||||
|
def _is_serif_run(run: dict[str, Any]) -> bool:
|
||||||
|
"""Return whether a text run uses a serif-like family."""
|
||||||
|
for family in str(run.get('font_family', '')).split(','):
|
||||||
|
name = family.strip().strip("'\"").lower()
|
||||||
|
if not name or name in {'sans-serif', 'sans serif'}:
|
||||||
|
continue
|
||||||
|
if name in _SERIF_WIDTH_FAMILIES:
|
||||||
|
return True
|
||||||
|
if 'serif' in name and 'sans' not in name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _estimate_run_text_width(run: dict[str, Any]) -> float:
|
||||||
|
"""Estimate one text run's rendered width, including tracking."""
|
||||||
|
text = str(run.get('text', ''))
|
||||||
|
base_width = estimate_text_width(
|
||||||
|
text,
|
||||||
|
float(run.get('font_size', 16)),
|
||||||
|
str(run.get('font_weight', '400')),
|
||||||
|
)
|
||||||
|
letter_spacing_px = float(run.get('letter_spacing', 0.0) or 0.0)
|
||||||
|
return base_width + letter_spacing_px * max(len(text) - 1, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _estimate_text_runs_width(runs: list[dict[str, Any]]) -> float:
|
||||||
|
"""Estimate a line of text runs with renderer headroom."""
|
||||||
|
width = sum(_estimate_run_text_width(run) for run in runs)
|
||||||
|
if any(_is_serif_run(run) for run in runs):
|
||||||
|
return width * 1.35
|
||||||
|
return width * 1.12
|
||||||
|
|
||||||
|
|
||||||
def _override_run_attrs(
|
def _override_run_attrs(
|
||||||
parent_attrs: dict[str, Any],
|
parent_attrs: dict[str, Any],
|
||||||
tspan: ET.Element,
|
tspan: ET.Element,
|
||||||
@@ -876,6 +960,12 @@ def _override_run_attrs(
|
|||||||
run_attrs['font_style'] = tspan.get('font-style')
|
run_attrs['font_style'] = tspan.get('font-style')
|
||||||
if tspan.get('text-decoration'):
|
if tspan.get('text-decoration'):
|
||||||
run_attrs['text_decoration'] = tspan.get('text-decoration')
|
run_attrs['text_decoration'] = tspan.get('text-decoration')
|
||||||
|
if tspan.get('letter-spacing'):
|
||||||
|
run_attrs['letter_spacing'] = _parse_letter_spacing_px(
|
||||||
|
tspan.get('letter-spacing'),
|
||||||
|
font_size=float(run_attrs.get('font_size', 16)),
|
||||||
|
scale_x=float(run_attrs.get('_scale_x', 1.0)),
|
||||||
|
)
|
||||||
return run_attrs
|
return run_attrs
|
||||||
|
|
||||||
|
|
||||||
@@ -916,8 +1006,8 @@ def _build_text_runs(
|
|||||||
"""Build a list of text runs from a <text> element, handling <tspan> children.
|
"""Build a list of text runs from a <text> element, handling <tspan> children.
|
||||||
|
|
||||||
Each run is a dict with keys: text, fill, fill_raw, font_weight,
|
Each run is a dict with keys: text, fill, fill_raw, font_weight,
|
||||||
font_style, font_family, font_size. Nested tspans are walked recursively so
|
font_style, font_family, font_size, letter_spacing. Nested tspans are walked
|
||||||
inline format changes inside a tspan still produce distinct runs.
|
recursively so inline format changes inside a tspan still produce distinct runs.
|
||||||
"""
|
"""
|
||||||
runs: list[dict[str, Any]] = []
|
runs: list[dict[str, Any]] = []
|
||||||
preserve_space = _preserves_space(elem)
|
preserve_space = _preserves_space(elem)
|
||||||
@@ -1003,6 +1093,7 @@ def _build_run_xml(
|
|||||||
fs_px = run.get('font_size', 16)
|
fs_px = run.get('font_size', 16)
|
||||||
fstyle = run.get('font_style', '')
|
fstyle = run.get('font_style', '')
|
||||||
ff = run.get('font_family', '')
|
ff = run.get('font_family', '')
|
||||||
|
letter_spacing_px = float(run.get('letter_spacing', 0.0) or 0.0)
|
||||||
opacity = run.get('opacity')
|
opacity = run.get('opacity')
|
||||||
|
|
||||||
text_dec = run.get('text_decoration', '')
|
text_dec = run.get('text_decoration', '')
|
||||||
@@ -1016,6 +1107,7 @@ def _build_run_xml(
|
|||||||
i_attr = ' i="1"' if fstyle == 'italic' else ''
|
i_attr = ' i="1"' if fstyle == 'italic' else ''
|
||||||
u_attr = ' u="sng"' if 'underline' in text_dec else ''
|
u_attr = ' u="sng"' if 'underline' in text_dec else ''
|
||||||
strike_attr = ' strike="sngStrike"' if 'line-through' in text_dec else ''
|
strike_attr = ' strike="sngStrike"' if 'line-through' in text_dec else ''
|
||||||
|
spc_attr = _letter_spacing_to_drawingml_spc(letter_spacing_px)
|
||||||
|
|
||||||
fonts = parse_font_family(ff) if ff else default_fonts
|
fonts = parse_font_family(ff) if ff else default_fonts
|
||||||
run_fonts = resolve_text_run_fonts(text, fonts)
|
run_fonts = resolve_text_run_fonts(text, fonts)
|
||||||
@@ -1027,7 +1119,7 @@ def _build_run_xml(
|
|||||||
space_attr = ' xml:space="preserve"' if text != text.strip() or ' ' in text else ''
|
space_attr = ' xml:space="preserve"' if text != text.strip() or ' ' in text else ''
|
||||||
|
|
||||||
return f'''<a:r>
|
return f'''<a:r>
|
||||||
<a:rPr lang="{lang}" sz="{sz}"{b_attr}{i_attr}{u_attr}{strike_attr} dirty="0">
|
<a:rPr lang="{lang}" sz="{sz}"{b_attr}{i_attr}{u_attr}{strike_attr}{spc_attr} dirty="0">
|
||||||
{outline_xml}
|
{outline_xml}
|
||||||
{fill_xml}
|
{fill_xml}
|
||||||
{effect_xml}
|
{effect_xml}
|
||||||
@@ -1055,6 +1147,11 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
|||||||
stroke_opacity = get_stroke_opacity(elem, ctx)
|
stroke_opacity = get_stroke_opacity(elem, ctx)
|
||||||
font_style = _get_attr(elem, 'font-style', ctx) or ''
|
font_style = _get_attr(elem, 'font-style', ctx) or ''
|
||||||
text_decoration = _get_attr(elem, 'text-decoration', ctx) or ''
|
text_decoration = _get_attr(elem, 'text-decoration', ctx) or ''
|
||||||
|
letter_spacing_px = _parse_letter_spacing_px(
|
||||||
|
_get_attr(elem, 'letter-spacing', ctx),
|
||||||
|
font_size=font_size,
|
||||||
|
scale_x=ctx.scale_x or 1.0,
|
||||||
|
)
|
||||||
|
|
||||||
fonts = parse_font_family(font_family_str)
|
fonts = parse_font_family(font_family_str)
|
||||||
|
|
||||||
@@ -1066,6 +1163,8 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
|||||||
'font_family': font_family_str,
|
'font_family': font_family_str,
|
||||||
'font_style': font_style,
|
'font_style': font_style,
|
||||||
'text_decoration': text_decoration,
|
'text_decoration': text_decoration,
|
||||||
|
'letter_spacing': letter_spacing_px,
|
||||||
|
'_scale_x': ctx.scale_x or 1.0,
|
||||||
'opacity': opacity,
|
'opacity': opacity,
|
||||||
'stroke_raw': stroke_raw,
|
'stroke_raw': stroke_raw,
|
||||||
'stroke_width': stroke_width,
|
'stroke_width': stroke_width,
|
||||||
@@ -1103,11 +1202,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
|||||||
if not line_runs:
|
if not line_runs:
|
||||||
continue
|
continue
|
||||||
visual_line_widths.append(
|
visual_line_widths.append(
|
||||||
estimate_text_width(
|
_estimate_text_runs_width(line_runs)
|
||||||
''.join(r['text'] for r in line_runs),
|
|
||||||
font_size,
|
|
||||||
font_weight,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
soft_break = child.get('data-paragraph-soft-break') == '1'
|
soft_break = child.get('data-paragraph-soft-break') == '1'
|
||||||
if soft_break and paragraph_runs:
|
if soft_break and paragraph_runs:
|
||||||
@@ -1164,7 +1259,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
|||||||
+ font_size * 1.5
|
+ font_size * 1.5
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
text_width = estimate_text_width(full_text, font_size, font_weight) * 1.05
|
text_width = _estimate_text_runs_width(runs)
|
||||||
text_height = font_size * 1.5
|
text_height = font_size * 1.5
|
||||||
padding = font_size * 0.1
|
padding = font_size * 0.1
|
||||||
|
|
||||||
@@ -1202,16 +1297,6 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
|||||||
box_x = ctx.translate_x + sx * (a * raw_box_x + e)
|
box_x = ctx.translate_x + sx * (a * raw_box_x + e)
|
||||||
box_y = ctx.translate_y + sy * (d * raw_box_y + f)
|
box_y = ctx.translate_y + sy * (d * raw_box_y + f)
|
||||||
|
|
||||||
# Letter spacing
|
|
||||||
spc_attr = ''
|
|
||||||
letter_spacing = _get_attr(elem, 'letter-spacing', ctx)
|
|
||||||
if letter_spacing:
|
|
||||||
try:
|
|
||||||
spc_val = float(letter_spacing) * 100
|
|
||||||
spc_attr = f' spc="{int(spc_val)}"'
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Text rotation. SVG's rotate(angle [cx cy]) rotates around (cx, cy), but
|
# Text rotation. SVG's rotate(angle [cx cy]) rotates around (cx, cy), but
|
||||||
# DrawingML's <a:xfrm rot="..."> rotates the shape around its own center.
|
# DrawingML's <a:xfrm rot="..."> rotates the shape around its own center.
|
||||||
# When a pivot is given (and differs from the box center), translate the
|
# When a pivot is given (and differs from the box center), translate the
|
||||||
|
|||||||
+162
-13
@@ -9,6 +9,8 @@ import os
|
|||||||
import re
|
import re
|
||||||
import posixpath
|
import posixpath
|
||||||
import shutil
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -33,7 +35,10 @@ from .pptx_media import (
|
|||||||
)
|
)
|
||||||
from .pptx_notes import (
|
from .pptx_notes import (
|
||||||
markdown_to_plain_text,
|
markdown_to_plain_text,
|
||||||
create_notes_slide_xml, create_notes_slide_rels_xml,
|
create_notes_master_rels_xml,
|
||||||
|
create_notes_master_xml,
|
||||||
|
create_notes_slide_xml,
|
||||||
|
create_notes_slide_rels_xml,
|
||||||
)
|
)
|
||||||
from .pptx_narration import (
|
from .pptx_narration import (
|
||||||
AUDIO_CONTENT_TYPES,
|
AUDIO_CONTENT_TYPES,
|
||||||
@@ -89,12 +94,33 @@ def _append_relationship(
|
|||||||
return next_rid
|
return next_rid
|
||||||
|
|
||||||
|
|
||||||
|
def _find_relationship_id(
|
||||||
|
rels_path: Path,
|
||||||
|
rel_type: str,
|
||||||
|
target: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""Find an existing relationship id by type and target."""
|
||||||
|
if not rels_path.exists():
|
||||||
|
return None
|
||||||
|
rels_content = rels_path.read_text(encoding='utf-8')
|
||||||
|
pattern = (
|
||||||
|
r'<Relationship\b[^>]*\bId="([^"]+)"[^>]*'
|
||||||
|
rf'\bType="{re.escape(rel_type)}"[^>]*'
|
||||||
|
rf'\bTarget="{re.escape(target)}"[^>]*/>'
|
||||||
|
)
|
||||||
|
match = re.search(pattern, rels_content)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
def _add_default_content_type(content_types: str, extension: str, content_type: str) -> str:
|
def _add_default_content_type(content_types: str, extension: str, content_type: str) -> str:
|
||||||
"""Add a Default content type if it is not already present."""
|
"""Add a Default content type if it is not already present."""
|
||||||
ext = extension.lstrip(".")
|
ext = extension.lstrip(".")
|
||||||
if f'Extension="{ext}"' in content_types:
|
if f'Extension="{ext}"' in content_types:
|
||||||
return content_types
|
return content_types
|
||||||
entry = f' <Default Extension="{ext}" ContentType="{content_type}"/>'
|
entry = f' <Default Extension="{ext}" ContentType="{content_type}"/>'
|
||||||
|
override_pos = content_types.find('<Override ')
|
||||||
|
if override_pos >= 0:
|
||||||
|
return content_types[:override_pos] + entry + '\n' + content_types[override_pos:]
|
||||||
return content_types.replace('</Types>', entry + '\n</Types>')
|
return content_types.replace('</Types>', entry + '\n</Types>')
|
||||||
|
|
||||||
|
|
||||||
@@ -167,6 +193,112 @@ def _create_writable_work_dir(output_path: Path) -> Path:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _relax_output_permissions(output_path: Path) -> list[str]:
|
||||||
|
"""Make exported files readable outside the sandbox owner where possible."""
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_mode = output_path.stat().st_mode
|
||||||
|
readable_mode = (
|
||||||
|
current_mode
|
||||||
|
| stat.S_IRUSR
|
||||||
|
| stat.S_IWUSR
|
||||||
|
| stat.S_IRGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
)
|
||||||
|
os.chmod(output_path, readable_mode)
|
||||||
|
except OSError as exc:
|
||||||
|
warnings.append(f"chmod skipped for {output_path}: {exc}")
|
||||||
|
|
||||||
|
if os.name != 'nt':
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
# Windows ACLs can remain sandbox-only even when the file mode looks sane.
|
||||||
|
# Grant the built-in Users SID read access; the SID avoids localization
|
||||||
|
# issues on non-English Windows installations.
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['icacls', str(output_path), '/grant', '*S-1-5-32-545:R'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except OSError as exc:
|
||||||
|
warnings.append(f"icacls skipped for {output_path}: {exc}")
|
||||||
|
else:
|
||||||
|
if result.returncode != 0:
|
||||||
|
message = (result.stderr or result.stdout or '').strip()
|
||||||
|
details = f": {message}" if message else ''
|
||||||
|
warnings.append(f"icacls failed for {output_path}{details}")
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
_NOTES_MASTER_REL_TYPE = (
|
||||||
|
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_notes_master(extract_dir: Path) -> None:
|
||||||
|
"""Create notesMaster parts and wire them into the presentation package."""
|
||||||
|
ppt_dir = extract_dir / 'ppt'
|
||||||
|
notes_masters_dir = ppt_dir / 'notesMasters'
|
||||||
|
notes_masters_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
notes_master_path = notes_masters_dir / 'notesMaster1.xml'
|
||||||
|
if not notes_master_path.exists():
|
||||||
|
notes_master_path.write_text(create_notes_master_xml(), encoding='utf-8')
|
||||||
|
|
||||||
|
theme_dir = ppt_dir / 'theme'
|
||||||
|
theme_dir.mkdir(exist_ok=True)
|
||||||
|
theme1_path = theme_dir / 'theme1.xml'
|
||||||
|
theme2_path = theme_dir / 'theme2.xml'
|
||||||
|
if not theme2_path.exists():
|
||||||
|
if theme1_path.exists():
|
||||||
|
shutil.copy2(theme1_path, theme2_path)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Cannot create notes theme: ppt/theme/theme1.xml is missing')
|
||||||
|
|
||||||
|
notes_master_rels_dir = notes_masters_dir / '_rels'
|
||||||
|
notes_master_rels_dir.mkdir(exist_ok=True)
|
||||||
|
notes_master_rels_path = notes_master_rels_dir / 'notesMaster1.xml.rels'
|
||||||
|
if not notes_master_rels_path.exists():
|
||||||
|
notes_master_rels_path.write_text(
|
||||||
|
create_notes_master_rels_xml(),
|
||||||
|
encoding='utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
presentation_rels_path = ppt_dir / '_rels' / 'presentation.xml.rels'
|
||||||
|
notes_master_rid = _find_relationship_id(
|
||||||
|
presentation_rels_path,
|
||||||
|
_NOTES_MASTER_REL_TYPE,
|
||||||
|
'notesMasters/notesMaster1.xml',
|
||||||
|
)
|
||||||
|
if notes_master_rid is None:
|
||||||
|
notes_master_rid = _append_relationship(
|
||||||
|
presentation_rels_path,
|
||||||
|
_NOTES_MASTER_REL_TYPE,
|
||||||
|
'notesMasters/notesMaster1.xml',
|
||||||
|
)
|
||||||
|
|
||||||
|
presentation_path = ppt_dir / 'presentation.xml'
|
||||||
|
presentation_xml = presentation_path.read_text(encoding='utf-8')
|
||||||
|
if '<p:notesMasterIdLst>' in presentation_xml:
|
||||||
|
return
|
||||||
|
notes_master_lst = (
|
||||||
|
f'<p:notesMasterIdLst><p:notesMasterId r:id="{notes_master_rid}"/>'
|
||||||
|
'</p:notesMasterIdLst>'
|
||||||
|
)
|
||||||
|
if '</p:sldMasterIdLst>' not in presentation_xml:
|
||||||
|
raise RuntimeError('presentation.xml is missing p:sldMasterIdLst')
|
||||||
|
presentation_xml = presentation_xml.replace(
|
||||||
|
'</p:sldMasterIdLst>',
|
||||||
|
'</p:sldMasterIdLst>' + notes_master_lst,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
presentation_path.write_text(presentation_xml, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
def _to_float(value: Any, default: float) -> float:
|
def _to_float(value: Any, default: float) -> float:
|
||||||
if value is None:
|
if value is None:
|
||||||
return default
|
return default
|
||||||
@@ -836,6 +968,8 @@ def create_pptx_with_native_svg(
|
|||||||
notes_content = notes.get(svg_stem, '') if notes else ''
|
notes_content = notes.get(svg_stem, '') if notes else ''
|
||||||
notes_text = markdown_to_plain_text(notes_content) if notes_content else ''
|
notes_text = markdown_to_plain_text(notes_content) if notes_content else ''
|
||||||
if notes_text:
|
if notes_text:
|
||||||
|
_ensure_notes_master(extract_dir)
|
||||||
|
|
||||||
notes_slides_dir = extract_dir / 'ppt' / 'notesSlides'
|
notes_slides_dir = extract_dir / 'ppt' / 'notesSlides'
|
||||||
notes_slides_dir.mkdir(exist_ok=True)
|
notes_slides_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
@@ -944,19 +1078,13 @@ def create_pptx_with_native_svg(
|
|||||||
with open(content_types_path, 'r', encoding='utf-8') as f:
|
with open(content_types_path, 'r', encoding='utf-8') as f:
|
||||||
content_types = f.read()
|
content_types = f.read()
|
||||||
|
|
||||||
types_to_add: list[str] = []
|
|
||||||
if not use_native_shapes:
|
if not use_native_shapes:
|
||||||
if 'Extension="svg"' not in content_types:
|
content_types = _add_default_content_type(content_types, 'svg', 'image/svg+xml')
|
||||||
types_to_add.append(' <Default Extension="svg" ContentType="image/svg+xml"/>')
|
|
||||||
for ext in sorted(image_exts_used):
|
for ext in sorted(image_exts_used):
|
||||||
if f'Extension="{ext}"' not in content_types:
|
content_types = _add_default_content_type(
|
||||||
types_to_add.append(
|
content_types,
|
||||||
f' <Default Extension="{ext}" ContentType="{_content_type_for_extension(ext)}"/>'
|
ext,
|
||||||
)
|
_content_type_for_extension(ext),
|
||||||
|
|
||||||
if types_to_add:
|
|
||||||
content_types = content_types.replace(
|
|
||||||
'</Types>', '\n'.join(types_to_add) + '\n</Types>',
|
|
||||||
)
|
)
|
||||||
with open(content_types_path, 'w', encoding='utf-8') as f:
|
with open(content_types_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(content_types)
|
f.write(content_types)
|
||||||
@@ -971,8 +1099,26 @@ def create_pptx_with_native_svg(
|
|||||||
with open(content_types_path, 'w', encoding='utf-8') as f:
|
with open(content_types_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(content_types)
|
f.write(content_types)
|
||||||
|
|
||||||
# Add notesSlides content types
|
# Add notes master / slides content types
|
||||||
if enable_notes and notes_slides_created:
|
if enable_notes and notes_slides_created:
|
||||||
|
notes_theme_override = (
|
||||||
|
' <Override PartName="/ppt/theme/theme2.xml" '
|
||||||
|
'ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>'
|
||||||
|
)
|
||||||
|
if notes_theme_override not in content_types:
|
||||||
|
content_types = content_types.replace(
|
||||||
|
'</Types>',
|
||||||
|
notes_theme_override + '\n</Types>',
|
||||||
|
)
|
||||||
|
notes_master_override = (
|
||||||
|
' <Override PartName="/ppt/notesMasters/notesMaster1.xml" '
|
||||||
|
'ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"/>'
|
||||||
|
)
|
||||||
|
if notes_master_override not in content_types:
|
||||||
|
content_types = content_types.replace(
|
||||||
|
'</Types>',
|
||||||
|
notes_master_override + '\n</Types>',
|
||||||
|
)
|
||||||
for i in sorted(notes_slides_created):
|
for i in sorted(notes_slides_created):
|
||||||
override = (
|
override = (
|
||||||
f' <Override PartName="/ppt/notesSlides/notesSlide{i}.xml" '
|
f' <Override PartName="/ppt/notesSlides/notesSlide{i}.xml" '
|
||||||
@@ -1006,6 +1152,7 @@ def create_pptx_with_native_svg(
|
|||||||
arcname = file_path.relative_to(extract_dir)
|
arcname = file_path.relative_to(extract_dir)
|
||||||
zf.write(file_path, arcname)
|
zf.write(file_path, arcname)
|
||||||
shutil.move(str(temp_output_path), str(output_path))
|
shutil.move(str(temp_output_path), str(output_path))
|
||||||
|
permission_warnings = _relax_output_permissions(output_path)
|
||||||
|
|
||||||
if conversion_trace_path and conversion_trace is not None:
|
if conversion_trace_path and conversion_trace is not None:
|
||||||
conversion_trace_path.parent.mkdir(parents=True, exist_ok=True)
|
conversion_trace_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1022,6 +1169,8 @@ def create_pptx_with_native_svg(
|
|||||||
if verbose:
|
if verbose:
|
||||||
print()
|
print()
|
||||||
print(f"[Done] Saved: {output_path}")
|
print(f"[Done] Saved: {output_path}")
|
||||||
|
for warning in permission_warnings:
|
||||||
|
print(f" [warn] {warning}")
|
||||||
if conversion_trace_path and conversion_trace is not None:
|
if conversion_trace_path and conversion_trace is not None:
|
||||||
print(f" Trace: {conversion_trace_path}")
|
print(f" Trace: {conversion_trace_path}")
|
||||||
print(f" Succeeded: {success_count}, Failed: {len(svg_files) - success_count}")
|
print(f" Succeeded: {success_count}, Failed: {len(svg_files) - success_count}")
|
||||||
|
|||||||
+99
-4
@@ -153,10 +153,105 @@ def create_notes_slide_rels_xml(slide_num: int) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Relationship file XML string.
|
Relationship file XML string.
|
||||||
"""
|
"""
|
||||||
# No notesMaster relationship: the base PPTX produced by python-pptx does
|
|
||||||
# not ship a notesMaster part, so referencing one here would create a
|
|
||||||
# dangling rels Target and PowerPoint reports the file as corrupt.
|
|
||||||
return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
return f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide{slide_num}.xml"/>
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>
|
||||||
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide{slide_num}.xml"/>
|
||||||
|
</Relationships>'''
|
||||||
|
|
||||||
|
|
||||||
|
def create_notes_master_xml() -> str:
|
||||||
|
"""Create a minimal PowerPoint-compatible notes master XML."""
|
||||||
|
return '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
||||||
|
<p:cSld>
|
||||||
|
<p:spTree>
|
||||||
|
<p:nvGrpSpPr>
|
||||||
|
<p:cNvPr id="1" name=""/>
|
||||||
|
<p:cNvGrpSpPr/>
|
||||||
|
<p:nvPr/>
|
||||||
|
</p:nvGrpSpPr>
|
||||||
|
<p:grpSpPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="0" y="0"/>
|
||||||
|
<a:ext cx="0" cy="0"/>
|
||||||
|
<a:chOff x="0" y="0"/>
|
||||||
|
<a:chExt cx="0" cy="0"/>
|
||||||
|
</a:xfrm>
|
||||||
|
</p:grpSpPr>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="2" name="Header Placeholder 1"/>
|
||||||
|
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||||
|
<p:nvPr><p:ph type="hdr" sz="quarter"/></p:nvPr>
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr/>
|
||||||
|
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
|
||||||
|
</p:sp>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="3" name="Date Placeholder 2"/>
|
||||||
|
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||||
|
<p:nvPr><p:ph type="dt" sz="half" idx="1"/></p:nvPr>
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr/>
|
||||||
|
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
|
||||||
|
</p:sp>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="4" name="Slide Image Placeholder 3"/>
|
||||||
|
<p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr>
|
||||||
|
<p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr>
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr/>
|
||||||
|
</p:sp>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="5" name="Notes Placeholder 4"/>
|
||||||
|
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||||
|
<p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr>
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr/>
|
||||||
|
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
|
||||||
|
</p:sp>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="6" name="Footer Placeholder 5"/>
|
||||||
|
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||||
|
<p:nvPr><p:ph type="ftr" sz="quarter" idx="4"/></p:nvPr>
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr/>
|
||||||
|
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
|
||||||
|
</p:sp>
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="7" name="Slide Number Placeholder 6"/>
|
||||||
|
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
|
||||||
|
<p:nvPr><p:ph type="sldNum" sz="quarter" idx="5"/></p:nvPr>
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr/>
|
||||||
|
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>
|
||||||
|
</p:sp>
|
||||||
|
</p:spTree>
|
||||||
|
</p:cSld>
|
||||||
|
<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"
|
||||||
|
accent1="accent1" accent2="accent2" accent3="accent3"
|
||||||
|
accent4="accent4" accent5="accent5" accent6="accent6"
|
||||||
|
hlink="hlink" folHlink="folHlink"/>
|
||||||
|
<p:hf/>
|
||||||
|
<p:notesStyle>
|
||||||
|
<a:lvl1pPr marL="0" algn="l">
|
||||||
|
<a:defRPr sz="1200" lang="en-US"/>
|
||||||
|
</a:lvl1pPr>
|
||||||
|
</p:notesStyle>
|
||||||
|
</p:notesMaster>'''
|
||||||
|
|
||||||
|
|
||||||
|
def create_notes_master_rels_xml() -> str:
|
||||||
|
"""Create notes master relationships XML."""
|
||||||
|
return '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme2.xml"/>
|
||||||
</Relationships>'''
|
</Relationships>'''
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "shadcn",
|
"sourceId": "shadcn",
|
||||||
"repo": "https://github.com/shadcn-ui/ui.git",
|
"repo": "https://github.com/shadcn-ui/ui.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "b59f68ecc5216e39d3f492e79b9767388ccd43db",
|
"commit": "549852bffcad7718b92bf40ea44b94a4e627511a",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": "skills/shadcn",
|
"sourcePath": "skills/shadcn",
|
||||||
"syncedAt": "2026-06-24T03:56:08Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "ui-ux-pro-max",
|
"sourceId": "ui-ux-pro-max",
|
||||||
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "a13b2a02fd58d8ad325fefd9d146f62708be8456",
|
"commit": "bdf1179bcf641cca49ee7a5f76df14c3015fd38c",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": ".claude/skills/ui-ux-pro-max",
|
"sourcePath": ".claude/skills/ui-ux-pro-max",
|
||||||
"syncedAt": "2026-06-24T03:56:08Z"
|
"syncedAt": "2026-06-24T15:59:59Z"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user