diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 541c34ba8..e600a7759 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -220,30 +220,6 @@ }, "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", "source": { @@ -280,6 +256,42 @@ }, "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", "source": { @@ -315,18 +327,6 @@ "authentication": "ON_INSTALL" }, "category": "MCP" - }, - { - "name": "design-md", - "source": { - "source": "local", - "path": "./plugins/codex/plugins/design-md" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "知识库与检索" } ] } diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index e4f1a1af2..569bd1fd3 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -24,8 +24,8 @@ "repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git", "ref": "main", "adapter": "claude-skill", - "commit": "a13b2a02fd58d8ad325fefd9d146f62708be8456", - "syncedAt": "2026-06-24T03:56:08Z" + "commit": "bdf1179bcf641cca49ee7a5f76df14c3015fd38c", + "syncedAt": "2026-06-24T15:59:59Z" }, { "id": "shadcn", @@ -33,8 +33,8 @@ "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", "adapter": "claude-skill", - "commit": "b59f68ecc5216e39d3f492e79b9767388ccd43db", - "syncedAt": "2026-06-24T03:56:08Z" + "commit": "549852bffcad7718b92bf40ea44b94a4e627511a", + "syncedAt": "2026-06-24T15:59:59Z" }, { "id": "frontend-slides", @@ -69,8 +69,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "54ef7c733480ca44061fc76d6d29dcf2227ecd67", - "syncedAt": "2026-06-24T03:56:08Z" + "commit": "7647212fabf12d16c2e90525d0f3f6229ec058a3", + "syncedAt": "2026-06-24T15:59:59Z" }, { "id": "next-skills", @@ -78,8 +78,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "16db9d05442fc1521c02e6690599aa89da0df0ec", - "syncedAt": "2026-06-24T03:48:19Z" + "commit": "72c26d1442602c9ab000354b7addb63841884a91", + "syncedAt": "2026-06-24T15:59:59Z" } ] } diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index 8e807f24f..31753e2c7 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -220,30 +220,6 @@ }, "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", "source": { @@ -280,6 +256,42 @@ }, "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", "source": { @@ -315,18 +327,6 @@ "authentication": "ON_INSTALL" }, "category": "MCP" - }, - { - "name": "design-md", - "source": { - "source": "local", - "path": "./plugins/design-md" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "知识库与检索" } ] } diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 0b1f14cf6..d69998f4e 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-06-24T03:57:12Z" + "syncedAt": "2026-06-24T16:01:19Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index c337d36db..3e3b1f694 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "16db9d05442fc1521c02e6690599aa89da0df0ec", + "commit": "72c26d1442602c9ab000354b7addb63841884a91", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-06-24T03:48:19Z" + "syncedAt": "2026-06-24T15:59:59Z" } diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md index 63a526c6d..2000368d9 100644 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-adoption/SKILL.md @@ -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. - 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 @@ -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 ``` -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. diff --git a/plugins/codex/plugins/ppt-master/README.md b/plugins/codex/plugins/ppt-master/README.md index 72d912997..8b56895e5 100644 --- a/plugins/codex/plugins/ppt-master/README.md +++ b/plugins/codex/plugins/ppt-master/README.md @@ -13,7 +13,7 @@ English | [中文](./README_CN.md)
-This project is kept free and open source with the support of PackyCode, APIKEY.FUN, RunAPI and other sponsors. +This project is kept free and open source with the support of PackyCode, APIKEY.FUN, RunAPI, YouYun ZhiSuan and other sponsors. @@ -28,6 +28,10 @@ English | [中文](./README_CN.md) + + + +
RunAPI 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 10% of official rates, 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 our dedicated link to claim ¥7 in free credit.
YouYun ZhiSuanThanks 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 this link to receive up to ¥10 in free credits and get started right away.
@@ -354,6 +358,8 @@ PPT Master is currently built and maintained primarily by me. Every new template   RunAPI   +YouYun ZhiSuanPowered by DigitalOcean **Individual support** diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index ed5b4b25a..dd28e86f7 100644 --- a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ppt-master", "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", - "commit": "54ef7c733480ca44061fc76d6d29dcf2227ecd67", + "commit": "7647212fabf12d16c2e90525d0f3f6229ec058a3", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-06-24T03:56:08Z" + "syncedAt": "2026-06-24T15:59:59Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py index c7044c25f..642409aba 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py @@ -831,7 +831,15 @@ def _char_width(ch: str, font_size: float, bold: bool) -> 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( @@ -849,7 +857,7 @@ def _find_break_point( for i in range(start, len(text)): 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 last_break > start: return last_break, last_break_w @@ -898,7 +906,6 @@ def _wrap_paragraph_into_lines( text = run.text i = 0 while i < len(text): - remaining = text[i:] avail = max_width - cur_w if avail <= 0 and lines[-1]: # Line is full; start a new one @@ -906,21 +913,21 @@ def _wrap_paragraph_into_lines( cur_w = 0.0 avail = max_width - end, used = _find_break_point(remaining, 0, avail, run) - if end == 0: + end, used = _find_break_point(text, i, avail, run) + if end == i: # Nothing fits even from a fresh line — force one char to avoid # an infinite loop. if lines[-1]: lines.append([]) cur_w = 0.0 continue - end = 1 - used = _char_width(remaining[0], run.font_size_px, run.bold) + end = i + 1 + used = _advance_width(text[i], 0, run) - chunk = remaining[:end] + chunk = text[i:end] lines[-1].append(_copy_run(run, text=chunk)) cur_w += used - i += end + i = end if i < len(text): # More to render — wrap to next line diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/embed_images.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/embed_images.py index 3c6d8aeb6..238503b99 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/embed_images.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_finalize/embed_images.py @@ -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: 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: """Run the CLI entry point.""" diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py index cd4d98eb4..895b84833 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml_elements.py @@ -821,6 +821,23 @@ def convert_polyline(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | Non # 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: """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' +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( parent_attrs: dict[str, Any], tspan: ET.Element, @@ -876,6 +960,12 @@ def _override_run_attrs( run_attrs['font_style'] = tspan.get('font-style') if 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 @@ -916,8 +1006,8 @@ def _build_text_runs( """Build a list of text runs from a element, handling children. 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 - inline format changes inside a tspan still produce distinct runs. + font_style, font_family, font_size, letter_spacing. Nested tspans are walked + recursively so inline format changes inside a tspan still produce distinct runs. """ runs: list[dict[str, Any]] = [] preserve_space = _preserves_space(elem) @@ -1003,6 +1093,7 @@ def _build_run_xml( fs_px = run.get('font_size', 16) fstyle = run.get('font_style', '') ff = run.get('font_family', '') + letter_spacing_px = float(run.get('letter_spacing', 0.0) or 0.0) opacity = run.get('opacity') text_dec = run.get('text_decoration', '') @@ -1016,6 +1107,7 @@ def _build_run_xml( i_attr = ' i="1"' if fstyle == 'italic' else '' u_attr = ' u="sng"' if 'underline' 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 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 '' return f''' - + {outline_xml} {fill_xml} {effect_xml} @@ -1055,6 +1147,11 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: stroke_opacity = get_stroke_opacity(elem, ctx) font_style = _get_attr(elem, 'font-style', 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) @@ -1066,6 +1163,8 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: 'font_family': font_family_str, 'font_style': font_style, 'text_decoration': text_decoration, + 'letter_spacing': letter_spacing_px, + '_scale_x': ctx.scale_x or 1.0, 'opacity': opacity, 'stroke_raw': stroke_raw, 'stroke_width': stroke_width, @@ -1103,11 +1202,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: if not line_runs: continue visual_line_widths.append( - estimate_text_width( - ''.join(r['text'] for r in line_runs), - font_size, - font_weight, - ) + _estimate_text_runs_width(line_runs) ) soft_break = child.get('data-paragraph-soft-break') == '1' if soft_break and paragraph_runs: @@ -1164,7 +1259,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: + font_size * 1.5 ) 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 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_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 # DrawingML's rotates the shape around its own center. # When a pivot is given (and differs from the box center), translate the diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_builder.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_builder.py index ce363c7b0..e38700968 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_builder.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_builder.py @@ -9,6 +9,8 @@ import os import re import posixpath import shutil +import stat +import subprocess import tempfile import uuid import zipfile @@ -33,7 +35,10 @@ from .pptx_media import ( ) from .pptx_notes import ( 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 ( AUDIO_CONTENT_TYPES, @@ -89,12 +94,33 @@ def _append_relationship( 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']*\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: """Add a Default content type if it is not already present.""" ext = extension.lstrip(".") if f'Extension="{ext}"' in content_types: return content_types entry = f' ' + override_pos = content_types.find('= 0: + return content_types[:override_pos] + entry + '\n' + content_types[override_pos:] return content_types.replace('', entry + '\n') @@ -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 '' in presentation_xml: + return + notes_master_lst = ( + f'' + '' + ) + if '' not in presentation_xml: + raise RuntimeError('presentation.xml is missing p:sldMasterIdLst') + presentation_xml = presentation_xml.replace( + '', + '' + notes_master_lst, + 1, + ) + presentation_path.write_text(presentation_xml, encoding='utf-8') + + def _to_float(value: Any, default: float) -> float: if value is None: return default @@ -836,6 +968,8 @@ def create_pptx_with_native_svg( notes_content = notes.get(svg_stem, '') if notes else '' notes_text = markdown_to_plain_text(notes_content) if notes_content else '' if notes_text: + _ensure_notes_master(extract_dir) + notes_slides_dir = extract_dir / 'ppt' / 'notesSlides' notes_slides_dir.mkdir(exist_ok=True) @@ -944,22 +1078,16 @@ def create_pptx_with_native_svg( with open(content_types_path, 'r', encoding='utf-8') as f: content_types = f.read() - types_to_add: list[str] = [] if not use_native_shapes: - if 'Extension="svg"' not in content_types: - types_to_add.append(' ') + content_types = _add_default_content_type(content_types, 'svg', 'image/svg+xml') for ext in sorted(image_exts_used): - if f'Extension="{ext}"' not in content_types: - types_to_add.append( - f' ' - ) - - if types_to_add: - content_types = content_types.replace( - '', '\n'.join(types_to_add) + '\n', + content_types = _add_default_content_type( + content_types, + ext, + _content_type_for_extension(ext), ) - with open(content_types_path, 'w', encoding='utf-8') as f: - f.write(content_types) + with open(content_types_path, 'w', encoding='utf-8') as f: + f.write(content_types) if audio_exts_used: for ext in sorted(audio_exts_used): @@ -971,8 +1099,26 @@ def create_pptx_with_native_svg( with open(content_types_path, 'w', encoding='utf-8') as f: f.write(content_types) - # Add notesSlides content types + # Add notes master / slides content types if enable_notes and notes_slides_created: + notes_theme_override = ( + ' ' + ) + if notes_theme_override not in content_types: + content_types = content_types.replace( + '', + notes_theme_override + '\n', + ) + notes_master_override = ( + ' ' + ) + if notes_master_override not in content_types: + content_types = content_types.replace( + '', + notes_master_override + '\n', + ) for i in sorted(notes_slides_created): override = ( f' str: Returns: 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''' - + + +''' + + +def create_notes_master_xml() -> str: + """Create a minimal PowerPoint-compatible notes master XML.""" + return ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + + +def create_notes_master_rels_xml() -> str: + """Create notes master relationships XML.""" + return ''' + + ''' diff --git a/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json index 5a301f8a8..f496c527b 100644 --- a/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "shadcn", "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", - "commit": "b59f68ecc5216e39d3f492e79b9767388ccd43db", + "commit": "549852bffcad7718b92bf40ea44b94a4e627511a", "adapter": "claude-skill", "sourcePath": "skills/shadcn", - "syncedAt": "2026-06-24T03:56:08Z" + "syncedAt": "2026-06-24T15:59:59Z" } diff --git a/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json index aec580c8e..60491d399 100644 --- a/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ui-ux-pro-max", "repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git", "ref": "main", - "commit": "a13b2a02fd58d8ad325fefd9d146f62708be8456", + "commit": "bdf1179bcf641cca49ee7a5f76df14c3015fd38c", "adapter": "claude-skill", "sourcePath": ".claude/skills/ui-ux-pro-max", - "syncedAt": "2026-06-24T03:56:08Z" + "syncedAt": "2026-06-24T15:59:59Z" }