pptx-reference-deck-analysis
Use when analyzing a reference PPTX for read-only structure, theme, typography, layout rhythm, diagnostics, derived template catalogs, or safe OOXML package inspection.
Install
npx skills add https://github.com/wshobson/agents/tree/main/plugins/pptx-deck-creation/skills/pptx-reference-deck-analysis
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
git clone https://github.com/wshobson/agents.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wshobson/agents collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
PPTX Reference Deck Analysis
Inspect a reference deck as design evidence. This skill never copies, clones, or mutates a source deck.
Contract
Implement required extraction on demand with a small task-local python-pptx script. Capture only the information required for the new deck:
- compact prompt context: slide count and size, text summaries, shape counts, styles, brand signals, template use, and layout rhythm;
- full extraction:
summary, slides, and read-onlylayout_treeevidence; - folder diagnostics: one result per deck plus a manifest;
- style-master analysis: colors, fonts, size distribution, master/layout use, and flow patterns;
- derived template catalog: zero-based source indices, layout roles, usable regions, placeholders, visual structures, and constraints.
Rules
- Keep the source deck read-only and independently author every target-slide coordinate.
- Use the bundled OOXML utilities only for raw themes, relationships, notes, comments, animations, media, masters, or layouts that high-level extraction cannot expose.
- Record inspected parts and parsing exceptions in the analysis manifest.
- Do not use extracted content, fonts, images, or proprietary assets in a generated deck without explicit permission and license evidence.
OOXML package inspection
Install defusedxml from requirements.txt before using the bundled utilities.
- Run
scripts/inspect.py <deck.pptx>for a compact JSON report of slide order, text, theme tokens, relationships, notes, comments, animations, and media. - Run
scripts/validate_package.py <deck.pptx> --output <report.json>for malformed XML, broken internal relationships, content-type gaps, duplicate layout links, and orphaned parts. - Run
scripts/unpack.py <deck.pptx> <output-dir>only when raw-package evidence is necessary. - Resolve relationship targets relative to the
.relsowner; never infer slide order from filenames.
Safety
- Never modify a supplied deck or blindly copy package parts into a new deck.
- Run the scripts from a trusted workspace; they reject path traversal, symlinks, oversized members, and archive bombs.
- Parse untrusted XML with
defusedxml; do not enable entity expansion, DTD loading, or network access. - Treat theme colors as tokens unless fully resolved against the color scheme.
See references/reference-deck-analysis.md for output shapes, references/reference-deck-analysis-patterns.md for documentation-only patterns, and references/ooxml-parsing.md for package part maps.
Files (agents)
-
references
-
ooxml-parsing.md 1.1 KB
# OOXML Parsing Reference A `.pptx` is an Open Packaging Conventions ZIP archive. Resolve its relationship graph; do not assume sequential filenames. | Need | Parts | | --- | --- | | Slide order | `ppt/presentation.xml`, `ppt/_rels/presentation.xml.rels` | | Slide text and shapes | Slide parts resolved from presentation relationships (commonly `ppt/slides/slideN.xml`) | | Layout, notes, images, charts | `ppt/slides/_rels/slideN.xml.rels` | | Template geometry | `ppt/slideLayouts/`, `ppt/slideMasters/` | | Colors and fonts | Theme parts resolved from presentation/master relationships (commonly under `ppt/theme/`) | | Notes and comments | `ppt/notesSlides/`, `ppt/comments/` | | Media and embeddings | `ppt/media/`, `ppt/embeddings/` | Use PresentationML, DrawingML, Office relationship, and package relationship namespaces. A read-only result should retain slide number, resolved relationship target, concatenated text, shape counts, notes, relationship types, and OOXML-only markers. Preserve theme tokens when a color uses scheme or system values rather than inventing a resolved RGB value. -
reference-deck-analysis-patterns.md 1.2 KB
# Reference-Deck Analysis Patterns Use these patterns only as documentation when writing a temporary analysis script with `python-pptx`; do not package them as an importable extraction library. ```python from collections.abc import Iterable, Iterator from typing import Any EMU_PER_INCH = 914400 def inches(value: int | None) -> float: return round(int(value or 0) / EMU_PER_INCH, 4) def bbox(shape: Any) -> dict[str, float]: return { "x": inches(getattr(shape, "left", 0)), "y": inches(getattr(shape, "top", 0)), "width": inches(getattr(shape, "width", 0)), "height": inches(getattr(shape, "height", 0)), } def iter_shapes(shapes: Iterable[Any]) -> Iterator[Any]: for shape in shapes: yield shape if hasattr(shape, "shapes"): yield from iter_shapes(shape.shapes) ``` For style analysis, count colors and fonts while recursively walking shapes. For extraction, create a root group that covers the slide, capture shape bboxes and kind-specific content, and resolve notes/media through package relationships when required. Wrap optional `python-pptx` properties in exception handling because fills, lines, colors, image data, tables, and charts can be absent or unsupported. -
reference-deck-analysis.md 1.5 KB
# Reference-Deck Analysis Recipes Inspect existing `.pptx` files only to derive evidence for a distinct deck. Do not ship runtime modules from this reference; implement a small task-local analysis when needed. ## Prompt context Return `slide_count`, `slide_size`, style and brand signals, template/layout evidence, and short title/text summaries with shape counts. ## Full extraction Return a read-only `summary`, per-slide `layout_tree` evidence, and OOXML markers required for inspection. Keep asset references and proprietary content out of a new deck unless explicitly approved. ## Style master Summarize palette, accent colors, typography, font-size distribution, master/layout usage, and dominant flow patterns. ## Derived reference-template catalog The catalog is a view over the analysis, not a copy plan. List every source slide by zero-based index and record its layout role, visual description, usable regions, placeholder roles, visual structures, and content-fit constraints. ```json { "source_deck": "reference.pptx", "slide_count": 12, "slides": [{ "source_index": 0, "layout_role": "cover", "description": "Dark cover with title and subtitle regions", "regions": ["title", "subtitle", "supporting visual"], "placeholder_roles": ["ctrTitle", "subTitle"], "visual_structures": ["full-bleed color field", "corner motif"], "reuse_constraints": ["best for one title and one short subtitle"] }] } ``` Use the catalog as inspiration, then author every target slide with independent coordinates.
-
-
scripts
-
inspect.py 5.9 KB
#!/usr/bin/env python3 """Read a PPTX OOXML package into a compact, JSON-safe inspection report.""" from __future__ import annotations import json import posixpath import stat import sys import zipfile from collections import Counter from pathlib import Path, PurePosixPath from defusedxml import ElementTree as ET P = "{http://schemas.openxmlformats.org/presentationml/2006/main}" A = "{http://schemas.openxmlformats.org/drawingml/2006/main}" R = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}" PR = "{http://schemas.openxmlformats.org/package/2006/relationships}" MAX_MEMBERS = 5_000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_SIZE = 512 * 1024 * 1024 MAX_COMPRESSION_RATIO = 1_000 def _write_stdout(value: str) -> None: """Write UTF-8 JSON without depending on the console code page.""" sys.stdout.buffer.write(value.encode("utf-8")) def _workspace_path(value: str) -> Path: root = Path.cwd().resolve() path = Path(value).expanduser().resolve() if not path.is_relative_to(root): raise ValueError(f"Path escapes the current workspace: {value}") return path def _validate_archive(archive: zipfile.ZipFile) -> None: members = archive.infolist() if len(members) > MAX_MEMBERS: raise ValueError("Archive contains too many entries") total = 0 for member in members: if stat.S_ISLNK(member.external_attr >> 16): raise ValueError(f"Archive contains a symlink: {member.filename}") if member.file_size > MAX_MEMBER_SIZE: raise ValueError(f"Archive entry is too large: {member.filename}") total += member.file_size if total > MAX_TOTAL_SIZE: raise ValueError("Archive uncompressed size is too large") if member.compress_size and member.file_size / member.compress_size > MAX_COMPRESSION_RATIO: raise ValueError(f"Suspicious compression ratio: {member.filename}") def _source_part(rels_name: str) -> str | None: path = PurePosixPath(rels_name) if str(path) == "_rels/.rels": return "" if path.parent.name != "_rels" or not path.name.endswith(".rels"): return None return str(path.parent.parent / path.name.removesuffix(".rels")) def _part_target(rels_name: str, target: str) -> str | None: source = _source_part(rels_name) if source is None: return None if target.startswith("/"): resolved = posixpath.normpath(target.lstrip("/")) else: resolved = posixpath.normpath(posixpath.join(posixpath.dirname(source), target)) return resolved if resolved not in {"", ".", ".."} and not resolved.startswith("../") else None def _xml(archive: zipfile.ZipFile, name: str): return ET.fromstring(archive.read(name)) def _relationships(archive: zipfile.ZipFile, name: str) -> dict[str, dict[str, str]]: if name not in archive.namelist(): return {} relationships = {} for item in _xml(archive, name).findall(f"{PR}Relationship"): mode = item.get("TargetMode", "Internal") target = item.get("Target", "") relationships[item.get("Id", "")] = { "type": item.get("Type", "").rsplit("/", 1)[-1], "target": target if mode == "External" else _part_target(name, target), "mode": mode, } return relationships def inspect(path: str) -> dict: with zipfile.ZipFile(path) as archive: _validate_archive(archive) names = set(archive.namelist()) presentation_rels = _relationships(archive, "ppt/_rels/presentation.xml.rels") presentation = _xml(archive, "ppt/presentation.xml") theme_name = "ppt/theme/theme1.xml" theme = _xml(archive, theme_name) if theme_name in names else None colors = [] if theme is None else [{"name": item.tag.removeprefix(A), "value": next((child.get("val") for child in item), None)} for item in theme.findall(f".//{A}clrScheme/*")] fonts = {} if theme is None else {key: node.get("typeface") for key, node in (("major_latin", theme.find(f".//{A}majorFont/{A}latin")), ("minor_latin", theme.find(f".//{A}minorFont/{A}latin"))) if node is not None} slides = [] for index, item in enumerate(presentation.findall(f".//{P}sldId"), start=1): rel = presentation_rels.get(item.get(f"{R}id", ""), {}) slide_name = rel.get("target", "") if slide_name not in names: slides.append({"index": index, "part": slide_name, "error": "missing slide part"}) continue slide = _xml(archive, slide_name) rels_name = f"{PurePosixPath(slide_name).parent}/_rels/{PurePosixPath(slide_name).name}.rels" rels = _relationships(archive, rels_name) text = "".join(node.text or "" for node in slide.findall(f".//{A}t")) shape_counts = Counter(node.tag.removeprefix(P) for node in slide.findall(f".//{P}spTree/*")) slides.append({"index": index, "part": slide_name, "slide_id": item.get("id"), "hidden": item.get("show") == "0", "text": text, "shape_counts": dict(shape_counts), "relationships": list(rels.values()), "has_transition": slide.find(f"{P}transition") is not None, "has_timing": slide.find(f"{P}timing") is not None}) return {"deck": path, "slide_count": len(slides), "theme": {"colors": colors, "fonts": fonts}, "slides": slides, "media": sorted(name for name in names if name.startswith("ppt/media/")), "notes_parts": sorted(name for name in names if name.startswith("ppt/notesSlides/notesSlide")), "comment_parts": sorted(name for name in names if "/comments" in name.lower())} def main(argv: list[str] | None = None) -> None: argv = sys.argv[1:] if argv is None else argv if len(argv) != 1: raise SystemExit("Usage: python inspect.py <deck.pptx>") path = _workspace_path(argv[0]) if not path.is_file(): raise SystemExit(f"Input package does not exist: {path}") _write_stdout(json.dumps(inspect(str(path)), ensure_ascii=False, indent=2) + "\n") if __name__ == "__main__": main() -
unpack.py 3 KB
#!/usr/bin/env python3 """Safely unpack an Office ZIP package and pretty-print XML for inspection.""" from __future__ import annotations import shutil import stat import sys import zipfile from pathlib import Path from defusedxml import minidom MAX_MEMBERS = 5_000 MAX_MEMBER_SIZE = 100 * 1024 * 1024 MAX_TOTAL_SIZE = 512 * 1024 * 1024 MAX_COMPRESSION_RATIO = 1_000 def _workspace_path(value: str) -> Path: root = Path.cwd().resolve() path = Path(value).expanduser().resolve() if not path.is_relative_to(root): raise ValueError(f"Path escapes the current workspace: {value}") return path def _validate_members(archive: zipfile.ZipFile, source: Path, output: Path) -> list[zipfile.ZipInfo]: members = archive.infolist() if len(members) > MAX_MEMBERS: raise ValueError("Archive contains too many entries") total = 0 for member in members: if stat.S_ISLNK(member.external_attr >> 16): raise ValueError(f"Archive contains a symlink: {member.filename}") target = (output / member.filename).resolve() if not target.is_relative_to(output): raise ValueError(f"Unsafe archive entry: {member.filename}") if target == source: raise ValueError(f"Archive entry would overwrite input package: {member.filename}") if member.file_size > MAX_MEMBER_SIZE: raise ValueError(f"Archive entry is too large: {member.filename}") total += member.file_size if total > MAX_TOTAL_SIZE: raise ValueError("Archive uncompressed size is too large") if member.compress_size and member.file_size / member.compress_size > MAX_COMPRESSION_RATIO: raise ValueError(f"Suspicious compression ratio: {member.filename}") return members def unpack(source: Path, output: Path) -> None: if output == source: raise ValueError("output directory must not be the input package") output.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(source) as archive: for member in _validate_members(archive, source, output): target = output / member.filename if member.is_dir(): target.mkdir(parents=True, exist_ok=True) continue target.parent.mkdir(parents=True, exist_ok=True) with archive.open(member) as reader, target.open("wb") as writer: shutil.copyfileobj(reader, writer) for path in [*output.rglob("*.xml"), *output.rglob("*.rels")]: document = minidom.parseString(path.read_bytes()) path.write_bytes(document.toprettyxml(indent=" ", encoding="utf-8")) def main(argv: list[str] | None = None) -> None: argv = sys.argv[1:] if argv is None else argv if len(argv) != 2: raise SystemExit("Usage: python unpack.py <office_file> <output_dir>") source, output = (_workspace_path(value) for value in argv) if not source.is_file(): raise SystemExit(f"Input package does not exist: {source}") unpack(source, output) if __name__ == "__main__": main() -
validate_package.py 9.5 KB
#!/usr/bin/env python3 """Validate PPTX package integrity without modifying the archive.""" from __future__ import annotations import argparse import json import posixpath import stat import sys import zipfile from collections import Counter from pathlib import Path, PurePosixPath from typing import Any from defusedxml import ElementTree as ET P = "{http://schemas.openxmlformats.org/presentationml/2006/main}" R = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}" PR = "{http://schemas.openxmlformats.org/package/2006/relationships}" CT = "{http://schemas.openxmlformats.org/package/2006/content-types}" SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" MAX_MEMBERS, MAX_MEMBER_SIZE, MAX_TOTAL_SIZE, MAX_COMPRESSION_RATIO = 5_000, 100 * 1024 * 1024, 512 * 1024 * 1024, 1_000 def _write_stdout(value: str) -> None: """Write UTF-8 JSON without depending on the console code page.""" sys.stdout.buffer.write(value.encode("utf-8")) def _workspace_path(value: str) -> Path: root = Path.cwd().resolve() path = Path(value).expanduser().resolve() if not path.is_relative_to(root): raise ValueError(f"Path escapes the current workspace: {value}") return path def _validate_archive(archive: zipfile.ZipFile) -> None: members, total = archive.infolist(), 0 if len(members) > MAX_MEMBERS: raise ValueError("Archive contains too many entries") for member in members: if stat.S_ISLNK(member.external_attr >> 16): raise ValueError(f"Archive contains a symlink: {member.filename}") if member.file_size > MAX_MEMBER_SIZE: raise ValueError(f"Archive entry is too large: {member.filename}") total += member.file_size if total > MAX_TOTAL_SIZE: raise ValueError("Archive uncompressed size is too large") if member.compress_size and member.file_size / member.compress_size > MAX_COMPRESSION_RATIO: raise ValueError(f"Suspicious compression ratio: {member.filename}") def _source_part(rels_name: str) -> str | None: path = PurePosixPath(rels_name) if str(path) == "_rels/.rels": return "" if path.parent.name != "_rels" or not path.name.endswith(".rels"): return None return str(path.parent.parent / path.name.removesuffix(".rels")) def _target(rels_name: str, value: str) -> str | None: source = _source_part(rels_name) if source is None: return None if value.startswith("/"): target = posixpath.normpath(value.lstrip("/")) else: target = posixpath.normpath(posixpath.join(posixpath.dirname(source), value)) return target if target not in {"", ".", ".."} and not target.startswith("../") else None def _content_types(archive: zipfile.ZipFile) -> tuple[dict[str, str], dict[str, str]]: root = ET.fromstring(archive.read("[Content_Types].xml")) return ( {item.get("Extension", "").lower(): item.get("ContentType", "") for item in root.findall(f"{CT}Default")}, {item.get("PartName", "").lstrip("/"): item.get("ContentType", "") for item in root.findall(f"{CT}Override")}, ) def validate(path: str) -> dict[str, Any]: errors: list[dict[str, str]] = [] warnings: list[dict[str, str]] = [] with zipfile.ZipFile(path) as archive: _validate_archive(archive) names = {item.filename for item in archive.infolist() if not item.is_dir()} for name in sorted(name for name in names if name.endswith((".xml", ".rels"))): try: ET.fromstring(archive.read(name)) except Exception as exc: errors.append({"part": name, "check": "xml_well_formed", "message": str(exc)}) if "[Content_Types].xml" not in names: errors.append({"part": "[Content_Types].xml", "check": "content_types", "message": "missing content types part"}) defaults, overrides = {}, {} else: try: defaults, overrides = _content_types(archive) except Exception as exc: errors.append({"part": "[Content_Types].xml", "check": "content_types", "message": str(exc)}) defaults, overrides = {}, {} referenced: set[str] = set() relationships: dict[str, dict[str, dict[str, str]]] = {} for rels_name in sorted(name for name in names if name.endswith(".rels")): try: root = ET.fromstring(archive.read(rels_name)) except Exception: continue rels: dict[str, dict[str, str]] = {} for rel in root.findall(f"{PR}Relationship"): rel_id, target, mode = rel.get("Id", ""), rel.get("Target", ""), rel.get("TargetMode", "Internal") rels[rel_id] = {"type": rel.get("Type", ""), "target": target, "mode": mode} if mode != "External": resolved = _target(rels_name, target) if not resolved or resolved not in names: errors.append({"part": rels_name, "check": "internal_relationship", "message": f"{rel_id} targets missing or unsafe part: {target}"}) else: referenced.add(resolved) relationships[rels_name] = rels presentation, declared_slides = "ppt/presentation.xml", set() presentation_rels = relationships.get("ppt/_rels/presentation.xml.rels", {}) if presentation not in names: errors.append({"part": presentation, "check": "slide_order", "message": "missing presentation part"}) else: try: root = ET.fromstring(archive.read(presentation)) ids = [item.get("id", "") for item in root.findall(f".//{P}sldId")] for value, count in Counter(ids).items(): if value and count > 1: errors.append({"part": presentation, "check": "slide_id_unique", "message": f"duplicate slide id: {value}"}) for item in root.findall(f".//{P}sldId"): rel_id = item.get(f"{R}id", "") rel = presentation_rels.get(rel_id) if rel is None or not rel["type"].endswith("/slide"): errors.append({"part": presentation, "check": "slide_relationship", "message": f"slide id references missing/non-slide relationship: {rel_id}"}) else: target = _target("ppt/_rels/presentation.xml.rels", rel["target"]) if target: declared_slides.add(target) except Exception as exc: errors.append({"part": presentation, "check": "slide_order", "message": str(exc)}) for slide in sorted(name for name in names if name.startswith("ppt/slides/slide") and name.endswith(".xml")): if overrides.get(slide) != SLIDE_CONTENT_TYPE: errors.append({"part": slide, "check": "content_type", "message": "missing or incorrect slide content type override"}) if slide not in declared_slides: warnings.append({"part": slide, "check": "unlisted_slide", "message": "slide part is not listed in presentation.xml"}) rels_name = f"{PurePosixPath(slide).parent}/_rels/{PurePosixPath(slide).name}.rels" layouts = [item for item in relationships.get(rels_name, {}).values() if item["type"].endswith("/slideLayout")] if len(layouts) != 1: errors.append({"part": rels_name, "check": "slide_layout_relationship", "message": f"expected exactly one slideLayout relationship, found {len(layouts)}"}) for check, candidates in {"orphaned_media": (item for item in names if item.startswith("ppt/media/")), "orphaned_notes": (item for item in names if item.startswith("ppt/notesSlides/notesSlide") and item.endswith(".xml"))}.items(): for name in sorted(candidates): if name not in referenced: warnings.append({"part": name, "check": check, "message": "part has no inbound internal relationship"}) return {"deck": path, "ok": not errors, "error_count": len(errors), "warning_count": len(warnings), "errors": errors, "warnings": warnings} def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Validate PPTX package integrity without modifying it.") parser.add_argument("deck", help="PPTX file inside the current workspace") parser.add_argument("--output", help="Optional JSON report path inside the current workspace") args = parser.parse_args(argv) try: deck = _workspace_path(args.deck) if not deck.is_file() or deck.suffix.lower() != ".pptx": raise ValueError("deck must be an existing .pptx file") report = validate(str(deck)) payload = json.dumps(report, ensure_ascii=False, indent=2) + "\n" if args.output: output = _workspace_path(args.output) if output == deck: raise ValueError("output must not overwrite the input deck") output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload, encoding="utf-8") _write_stdout(payload) return 0 if report["ok"] else 1 except (OSError, ValueError, zipfile.BadZipFile) as exc: _write_stdout( json.dumps( {"ok": False, "errors": [{"check": "input", "message": str(exc)}]}, ensure_ascii=False, indent=2, ) + "\n" ) return 2 if __name__ == "__main__": raise SystemExit(main())
-
-
requirements.txt 19 B
defusedxml>=0.7,<1 -
SKILL.md 2.7 KB
--- name: pptx-reference-deck-analysis description: "Use when analyzing a reference PPTX for read-only structure, theme, typography, layout rhythm, diagnostics, derived template catalogs, or safe OOXML package inspection." --- # PPTX Reference Deck Analysis Inspect a reference deck as design evidence. This skill never copies, clones, or mutates a source deck. ## Contract Implement required extraction on demand with a small task-local `python-pptx` script. Capture only the information required for the new deck: - compact prompt context: slide count and size, text summaries, shape counts, styles, brand signals, template use, and layout rhythm; - full extraction: `summary`, slides, and read-only `layout_tree` evidence; - folder diagnostics: one result per deck plus a manifest; - style-master analysis: colors, fonts, size distribution, master/layout use, and flow patterns; - derived template catalog: zero-based source indices, layout roles, usable regions, placeholders, visual structures, and constraints. ## Rules - Keep the source deck read-only and independently author every target-slide coordinate. - Use the bundled OOXML utilities only for raw themes, relationships, notes, comments, animations, media, masters, or layouts that high-level extraction cannot expose. - Record inspected parts and parsing exceptions in the analysis manifest. - Do not use extracted content, fonts, images, or proprietary assets in a generated deck without explicit permission and license evidence. ## OOXML package inspection Install `defusedxml` from `requirements.txt` before using the bundled utilities. 1. Run `scripts/inspect.py <deck.pptx>` for a compact JSON report of slide order, text, theme tokens, relationships, notes, comments, animations, and media. 2. Run `scripts/validate_package.py <deck.pptx> --output <report.json>` for malformed XML, broken internal relationships, content-type gaps, duplicate layout links, and orphaned parts. 3. Run `scripts/unpack.py <deck.pptx> <output-dir>` only when raw-package evidence is necessary. 4. Resolve relationship targets relative to the `.rels` owner; never infer slide order from filenames. ### Safety - Never modify a supplied deck or blindly copy package parts into a new deck. - Run the scripts from a trusted workspace; they reject path traversal, symlinks, oversized members, and archive bombs. - Parse untrusted XML with `defusedxml`; do not enable entity expansion, DTD loading, or network access. - Treat theme colors as tokens unless fully resolved against the color scheme. See `references/reference-deck-analysis.md` for output shapes, `references/reference-deck-analysis-patterns.md` for documentation-only patterns, and `references/ooxml-parsing.md` for package part maps.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.