mirror of
https://github.com/anthropics/skills.git
synced 2026-08-02 21:15:27 +08:00
fa0fa64bdc
Add support for template formats (.dotx, .potx, .xltx) across validation and the helper scripts. Consolidate the shared office helpers into a single module, replacing the pack/unpack pipeline with explicit zip/unzip steps. Extraction now rejects symlink and path-traversal archive entries. Move run merging to a standalone docx script, since it only ever applied to Word documents. Fix the redlining validator so it compares against the original even when a document has no tracked changes, which is when an untracked edit would otherwise go unreported. Provision a LibreOffice user profile per invocation so conversions work in sandboxed environments. Trim the skill docs to the guidance that earns its place.
115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
"""Find masters sharing a theme part in the way PowerPoint refuses to open.
|
|
|
|
Reports only; the fix is to move <p:notesMasterIdLst> back to directly after
|
|
<p:sldIdLst> in ppt/presentation.xml.
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
import posixpath
|
|
import re
|
|
from typing import Mapping
|
|
|
|
from . import part_text
|
|
|
|
THEME_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
|
|
|
_MASTER_RE = re.compile(
|
|
r"^ppt/(?P<group>slideMasters|notesMasters|handoutMasters)/"
|
|
r"(?:slide|notes|handout)Master(?P<num>\d+)\.xml$"
|
|
)
|
|
_GROUP_ORDER = {"slideMasters": 0, "notesMasters": 1, "handoutMasters": 2}
|
|
|
|
_RELATIONSHIP_RE = re.compile(
|
|
r"<Relationship\b[^>]*?(?:/>|>.*?</Relationship\s*>)", re.DOTALL
|
|
)
|
|
|
|
|
|
def _sort_key(name: str) -> tuple[int, int]:
|
|
m = _MASTER_RE.match(name)
|
|
assert m is not None
|
|
return (_GROUP_ORDER[m.group("group")], int(m.group("num")))
|
|
|
|
|
|
def _rels_path(part: str) -> str:
|
|
directory, base = posixpath.split(part)
|
|
return f"{directory}/_rels/{base}.rels"
|
|
|
|
|
|
def _resolve(rels_path: str, target: str) -> str:
|
|
if target.startswith("/"):
|
|
return target.lstrip("/")
|
|
part_dir = posixpath.dirname(posixpath.dirname(rels_path))
|
|
return posixpath.normpath(posixpath.join(part_dir, target))
|
|
|
|
|
|
def _theme_rel(files: Mapping[str, bytes], master: str):
|
|
rels_path = _rels_path(master)
|
|
rels = files.get(rels_path)
|
|
if rels is None:
|
|
return None
|
|
for element in _RELATIONSHIP_RE.findall(part_text(rels)):
|
|
if f'Type="{THEME_REL_TYPE}"' not in element:
|
|
continue
|
|
target = re.search(r'\bTarget="([^"]+)"', element)
|
|
if target is None:
|
|
continue
|
|
return rels_path, element, _resolve(rels_path, target.group(1))
|
|
return None
|
|
|
|
|
|
def _masters(files: Mapping[str, bytes]) -> list[str]:
|
|
return sorted((n for n in files if _MASTER_RE.match(n)), key=_sort_key)
|
|
|
|
|
|
_PRESENTATION = "ppt/presentation.xml"
|
|
_NOTES_MASTERS = "ppt/notesMasters/"
|
|
_IGNORABLE_RE = re.compile(r"<!--.*?-->|<\?.*?\?>", re.DOTALL)
|
|
_AFTER_SLDIDLST_RE = re.compile(
|
|
r"<p:sldIdLst\b(?:[^>]*/>|[^>]*>.*?</p:sldIdLst\s*>)\s*(<[^>\s/]+)", re.DOTALL
|
|
)
|
|
|
|
|
|
def _notes_master_share_is_inert(files: Mapping[str, bytes]) -> bool:
|
|
data = files.get(_PRESENTATION)
|
|
if data is None:
|
|
return False
|
|
match = _AFTER_SLDIDLST_RE.search(_IGNORABLE_RE.sub("", part_text(data)))
|
|
return match is not None and match.group(1) == "<p:notesMasterIdLst"
|
|
|
|
|
|
def _shares(files: Mapping[str, bytes]):
|
|
owner: dict[str, str] = {}
|
|
for master in _masters(files):
|
|
found = _theme_rel(files, master)
|
|
if found is None:
|
|
continue
|
|
rels_path, element, theme = found
|
|
if theme not in files:
|
|
continue
|
|
if theme in owner:
|
|
yield master, rels_path, element, theme, owner[theme]
|
|
else:
|
|
owner[theme] = master
|
|
|
|
|
|
def _is_inert(master: str, inert_notes: bool) -> bool:
|
|
return inert_notes and master.startswith(_NOTES_MASTERS)
|
|
|
|
|
|
def find_shared_master_themes(files: Mapping[str, bytes]) -> list[str]:
|
|
return [
|
|
f"{master} shares {theme} with {first}"
|
|
for master, _, _, theme, first in _shares(files)
|
|
]
|
|
|
|
|
|
def live_shared_master_themes(files: Mapping[str, bytes]) -> list[str]:
|
|
inert_notes = _notes_master_share_is_inert(files)
|
|
return [
|
|
f"{master} shares {theme} with {first}"
|
|
for master, _, _, theme, first in _shares(files)
|
|
if not _is_inert(master, inert_notes)
|
|
]
|