Files

852 lines
30 KiB
Python

#!/usr/bin/env python3
"""
PPT Master - Authored Preset Shape Contract
Build and validate compact canonical SVG groups for newly authored PowerPoint
preset shapes while retaining expanded authored input compatibility.
Usage:
Import render_preset_shape_fragment or validate_authored_preset_group.
Examples:
fragment = render_preset_shape_fragment(
"rightArrow",
(80, 120, 240, 96),
element_id="next-step",
style={"fill": "#2563EB", "stroke": "none"},
)
Dependencies:
None (only uses standard library and local PPT Master modules)
"""
from __future__ import annotations
import math
import re
from typing import Mapping
from xml.etree import ElementTree as ET
from pptx_shapes import (
CONNECTOR_PRESET_TYPES,
OOXML_COORDINATE_MAX,
OOXML_COORDINATE_MIN,
SUPPORTED_OPERATORS,
get_preset_registry,
resolve_preset_preview_hash,
svg_preset_preview_fingerprint,
validate_ooxml_line_width,
validate_ooxml_xfrm,
)
from .emu_units import EMU_PER_PX, Xfrm, fmt_num
from .preset_registry_to_svg import render_preset_geometry
from .preset_svg_markup import (
attrs_to_xml,
serialize_compact_preset_layers,
serialize_preset_layers,
)
AUTHORING_ATTR = "data-pptx-authoring"
AUTHORING_VALUE = "preset"
_SVG_NAMESPACE = "http://www.w3.org/2000/svg"
_ID_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_.:-]*")
_PAINT_RE = re.compile(r"(?:none|#[0-9A-Fa-f]{6})")
_INTEGER_RE = re.compile(r"[+-]?\d+")
_ADJUSTMENT_PREFIX = "data-pptx-av-"
_STYLE_ATTRS = (
"fill",
"fill-opacity",
"stroke",
"stroke-linecap",
"stroke-linejoin",
"stroke-opacity",
"stroke-width",
)
_SEMANTIC_ATTRS = (
AUTHORING_ATTR,
"data-pptx-object",
"data-pptx-prst",
"data-pptx-frame",
)
_TEMPLATE_ATOM_ATTRS = (
"data-pptx-layer",
"data-pptx-editable",
"data-pptx-carrier",
"data-pptx-role",
)
def render_preset_shape_fragment(
preset: str,
frame: tuple[float, float, float, float],
*,
adjustments: Mapping[str, str | int | float] | None = None,
object_kind: str = "shape",
element_id: str,
name: str | None = None,
style: Mapping[str, str] | None = None,
) -> str:
"""Render one compact authored preset fragment for SVG insertion."""
registry = get_preset_registry()
if preset not in registry:
raise ValueError(f"Unknown DrawingML preset shape: {preset!r}")
if _ID_RE.fullmatch(element_id) is None:
raise ValueError(f"Invalid SVG element id: {element_id!r}")
if object_kind not in {"shape", "connector"}:
raise ValueError("object_kind must be 'shape' or 'connector'")
if preset in CONNECTOR_PRESET_TYPES and object_kind != "connector":
raise ValueError(
f"Connector preset {preset!r} requires object_kind='connector'"
)
if object_kind == "connector" and preset not in CONNECTOR_PRESET_TYPES:
raise ValueError(
f"Authored connector requires a connector preset, got {preset!r}"
)
x, y, width, height = _validate_frame(frame, object_kind)
adjustment_values = _normalize_adjustments(adjustments or {})
_validate_adjustments(preset, adjustment_values)
registry.evaluate(
preset,
width,
height,
adjustments=adjustment_values,
)
rendered = render_preset_geometry(
preset,
Xfrm(x=x, y=y, w=width, h=height),
adjustment_values,
)
if not rendered.paths:
raise ValueError(f"Preset {preset!r} produced no visible SVG paths")
frame_text = " ".join(
fmt_num(value, 8) for value in (x, y, width, height)
)
semantic_attrs = {
AUTHORING_ATTR: AUTHORING_VALUE,
"data-pptx-object": object_kind,
"data-pptx-prst": preset,
"data-pptx-frame": frame_text,
}
if name:
semantic_attrs["data-pptx-shape-name"] = name
for guide_name, formula in adjustment_values.items():
semantic_attrs[f"{_ADJUSTMENT_PREFIX}{guide_name}"] = str(formula)
raw_style = dict(style or {})
if "fill" not in raw_style or "stroke" not in raw_style:
raise ValueError(
"Compact authored preset requires explicit local fill and stroke"
)
style_attrs = _validate_style(raw_style)
if (
style_attrs.get("stroke", "none") != "none"
and "stroke-width" not in style_attrs
):
style_attrs["stroke-width"] = "1"
if object_kind == "connector":
if style_attrs.get("fill", "none") != "none":
raise ValueError("Authored connector fill must be none")
if not _has_visible_stroke(style_attrs):
raise ValueError("Authored connector requires a visible stroke")
group_attrs = {
"id": element_id,
**semantic_attrs,
**style_attrs,
}
return (
f'<g{attrs_to_xml(group_attrs)}>\n'
f"{serialize_compact_preset_layers(rendered.paths, style_attrs)}\n"
"</g>"
)
def authored_preset_encoding(group: ET.Element) -> str | None:
"""Return ``compact`` / ``expanded`` for an authored preset group."""
if (
_local_name(group.tag) != "g"
or group.get(AUTHORING_ATTR) != AUTHORING_VALUE
):
return None
parts = {
child.get("data-pptx-part")
for child in group
if child.get("data-pptx-part") is not None
}
if parts:
return "expanded"
return "compact"
def validate_authored_preset_group(group: ET.Element) -> list[str]:
"""Return authored-preset contract errors for one logical group."""
encoding = authored_preset_encoding(group)
if encoding is None:
return []
if encoding == "compact":
return _validate_compact_authored_preset_group(group)
return _validate_expanded_authored_preset_group(group)
def _validate_compact_authored_preset_group(group: ET.Element) -> list[str]:
"""Validate the project-canonical compact authored-preset form."""
errors: list[str] = []
if not _is_svg_element(group, "g"):
return [f'{AUTHORING_ATTR}="{AUTHORING_VALUE}" requires an SVG <g>']
element_id = group.get("id")
if element_id is None:
errors.append("Authored preset logical group requires a stable id")
elif _ID_RE.fullmatch(element_id) is None:
errors.append(f"Authored preset logical group has invalid id {element_id!r}")
unexpected_group_attrs = sorted(
name for name in group.attrib
if _is_unexpected_group_attr(name, compact=True)
)
if unexpected_group_attrs:
errors.append(
"Authored preset logical group has unsupported attributes: "
+ ", ".join(unexpected_group_attrs)
)
if group.get("data-pptx-preview-sha256") is not None:
errors.append(
"Compact authored preset derives preview integrity from the registry; "
"remove data-pptx-preview-sha256"
)
if (group.text or "").strip():
errors.append("Compact authored preset cannot contain text content")
direct_children = list(group)
if not direct_children:
errors.append("Compact authored preset requires visible direct path layers")
return errors
if any(child.get("data-pptx-part") is not None for child in direct_children):
errors.append(
"Compact authored preset cannot mix transport carrier/preview markers"
)
return errors
if any(
not _is_svg_element(child, "path")
for child in direct_children
):
errors.append(
"Compact authored preset is atomic and may contain only direct SVG paths"
)
return errors
if any(list(child) for child in direct_children):
errors.append("Compact authored preset paths cannot contain child elements")
if any(
(child.text or "").strip() or (child.tail or "").strip()
for child in direct_children
):
errors.append(
"Compact authored preset paths may contain only whitespace around markup"
)
preset = group.get("data-pptx-prst") or ""
object_kind = group.get("data-pptx-object") or ""
if object_kind not in {"shape", "connector"}:
errors.append(
"Authored preset data-pptx-object must be 'shape' or 'connector'"
)
if preset in CONNECTOR_PRESET_TYPES and object_kind != "connector":
errors.append(
f"Connector preset {preset!r} requires data-pptx-object='connector'"
)
if object_kind == "connector" and preset not in CONNECTOR_PRESET_TYPES:
errors.append(
f"Authored connector requires a connector preset, got {preset!r}"
)
try:
frame = _parse_frame(group.get("data-pptx-frame"), object_kind)
canonical_frame = " ".join(fmt_num(value, 8) for value in frame)
if group.get("data-pptx-frame") != canonical_frame:
raise ValueError(
"Compact authored preset data-pptx-frame must use the helper's "
f"canonical spelling {canonical_frame!r}"
)
adjustments = {
name[len(_ADJUSTMENT_PREFIX):]: value
for name, value in group.attrib.items()
if name.startswith(_ADJUSTMENT_PREFIX)
}
_validate_adjustments(preset, adjustments)
rendered = render_preset_geometry(
preset,
Xfrm(x=frame[0], y=frame[1], w=frame[2], h=frame[3]),
adjustments,
)
raw_style = {
name: group.attrib[name]
for name in _STYLE_ATTRS
if name in group.attrib
}
if "fill" not in raw_style or "stroke" not in raw_style:
raise ValueError(
"Compact authored preset requires explicit local fill and stroke"
)
style_attrs = _validate_style(raw_style)
noncanonical_style = sorted(
name for name, value in style_attrs.items()
if raw_style.get(name) != value
)
if noncanonical_style:
raise ValueError(
"Compact authored preset style uses non-canonical values: "
+ ", ".join(noncanonical_style)
)
if style_attrs.get("stroke", "none") != "none" and (
"stroke-width" not in style_attrs
):
raise ValueError(
"Compact authored preset with a visible stroke requires stroke-width"
)
if object_kind == "connector":
if style_attrs.get("fill", "none") != "none":
raise ValueError("Authored connector fill must be none")
if not _has_visible_stroke(style_attrs):
raise ValueError("Authored connector requires a visible stroke")
expected_markup = serialize_compact_preset_layers(
rendered.paths,
style_attrs,
)
expected_root = ET.fromstring(
f'<g xmlns="http://www.w3.org/2000/svg">{expected_markup}</g>'
)
except (ET.ParseError, ValueError) as exc:
errors.append(f"Cannot regenerate compact authored preset: {exc}")
return errors
expected_children = list(expected_root)
if len(direct_children) != len(expected_children):
errors.append(
"Compact authored preset path count differs from registry output: "
f"expected {len(expected_children)}, found {len(direct_children)}"
)
return errors
for index, (actual, expected) in enumerate(
zip(direct_children, expected_children),
start=1,
):
if actual.attrib != expected.attrib:
errors.append(
f"Compact authored preset path {index} differs from registry output"
)
return errors
def _validate_expanded_authored_preset_group(group: ET.Element) -> list[str]:
"""Validate the legacy expanded authored-preset compatibility form."""
if group.get(AUTHORING_ATTR) != AUTHORING_VALUE:
return []
errors: list[str] = []
if not _is_svg_element(group, "g"):
return [f'{AUTHORING_ATTR}="{AUTHORING_VALUE}" requires an SVG <g>']
element_id = group.get("id")
if element_id is None:
errors.append("Authored preset logical group requires a stable id")
elif _ID_RE.fullmatch(element_id) is None:
errors.append(f"Authored preset logical group has invalid id {element_id!r}")
unexpected_group_attrs = sorted(
name for name in group.attrib
if _is_unexpected_group_attr(name, compact=False)
)
if unexpected_group_attrs:
errors.append(
"Authored preset logical group has unsupported attributes: "
+ ", ".join(unexpected_group_attrs)
)
direct_children = list(group)
carriers = [
child
for child in direct_children
if child.get("data-pptx-part") == "geometry"
]
previews = [
child
for child in direct_children
if child.get("data-pptx-part") == "geometry-preview"
]
if len(carriers) != 1:
errors.append(
f"Authored preset requires exactly one direct geometry carrier; "
f"found {len(carriers)}"
)
if len(previews) != 1:
errors.append(
f"Authored preset requires exactly one direct geometry preview; "
f"found {len(previews)}"
)
allowed_children = set(carriers + previews)
foreign_children = [
child for child in direct_children
if child not in allowed_children
]
if foreign_children:
errors.append(
"Authored preset groups are atomic; place labels or decorations "
"in a parent group"
)
if len(carriers) != 1 or len(previews) != 1:
return errors
carrier = carriers[0]
preview = previews[0]
if not _is_svg_element(carrier, "path"):
errors.append("Authored preset geometry carrier must be an SVG <path>")
if not _is_svg_element(preview, "g"):
errors.append("Authored preset geometry preview must be an SVG <g>")
if carrier.get("visibility") != "hidden":
errors.append('Authored preset carrier requires visibility="hidden"')
if carrier.get("pointer-events") != "none":
errors.append('Authored preset carrier requires pointer-events="none"')
for attr_name in _SEMANTIC_ATTRS:
if group.get(attr_name) != carrier.get(attr_name):
errors.append(
f"Authored preset group/carrier {attr_name} values differ"
)
adjustment_names = {
name
for element in (group, carrier)
for name in element.attrib
if name.startswith(_ADJUSTMENT_PREFIX)
}
for attr_name in sorted(adjustment_names):
if group.get(attr_name) != carrier.get(attr_name):
errors.append(
f"Authored preset group/carrier {attr_name} values differ"
)
unexpected_carrier_attrs = [
name
for name in carrier.attrib
if _is_unexpected_carrier_attr(name)
]
if unexpected_carrier_attrs:
errors.append(
"Authored preset carrier has unsupported presentation attributes: "
+ ", ".join(sorted(unexpected_carrier_attrs))
)
preset = carrier.get("data-pptx-prst") or ""
object_kind = carrier.get("data-pptx-object") or ""
if object_kind not in {"shape", "connector"}:
errors.append(
"Authored preset data-pptx-object must be 'shape' or 'connector'"
)
if preset in CONNECTOR_PRESET_TYPES and object_kind != "connector":
errors.append(
f"Connector preset {preset!r} requires data-pptx-object='connector'"
)
if object_kind == "connector" and preset not in CONNECTOR_PRESET_TYPES:
errors.append(
f"Authored connector requires a connector preset, got {preset!r}"
)
try:
frame = _parse_frame(carrier.get("data-pptx-frame"), object_kind)
adjustments = {
name[len(_ADJUSTMENT_PREFIX):]: value
for name, value in carrier.attrib.items()
if name.startswith(_ADJUSTMENT_PREFIX)
}
_validate_adjustments(preset, adjustments)
rendered = render_preset_geometry(
preset,
Xfrm(x=frame[0], y=frame[1], w=frame[2], h=frame[3]),
adjustments,
)
style_attrs = _validate_style({
name: carrier.attrib[name]
for name in _STYLE_ATTRS
if name in carrier.attrib
})
if object_kind == "connector":
if style_attrs.get("fill", "none") != "none":
raise ValueError("Authored connector fill must be none")
if not _has_visible_stroke(style_attrs):
raise ValueError("Authored connector requires a visible stroke")
expected = serialize_preset_layers(
rendered.paths,
{
name: value
for name, value in carrier.attrib.items()
if name in _SEMANTIC_ATTRS
or name.startswith(_ADJUSTMENT_PREFIX)
or name == "data-pptx-shape-name"
},
style_attrs,
)
except ValueError as exc:
errors.append(f"Cannot regenerate authored preset preview: {exc}")
return errors
if (carrier.get("d") or "").strip() != _carrier_path(rendered.paths):
errors.append("Authored preset carrier path differs from registry output")
actual_preview_hash = svg_preset_preview_fingerprint(group)
if actual_preview_hash != expected.preview_hash:
errors.append("Authored preset visible preview differs from registry output")
try:
stored_hash = resolve_preset_preview_hash(group)
except ValueError as exc:
errors.append(f"Invalid authored preset preview fingerprint: {exc}")
else:
if stored_hash != expected.preview_hash:
errors.append(
"Authored preset fingerprint does not match regenerated metadata"
)
return errors
def validate_authored_preset_tree(root: ET.Element) -> list[str]:
"""Return structural errors for every authored preset marker in one SVG."""
errors: list[str] = []
id_counts: dict[str, int] = {}
for element in root.iter():
element_id = element.get("id")
if element_id:
id_counts[element_id] = id_counts.get(element_id, 0) + 1
parents = {
child: parent
for parent in root.iter()
for child in parent
}
for element in root.iter():
authoring = element.get(AUTHORING_ATTR)
if authoring is None:
continue
tag = _local_name(element.tag)
label = _element_label(element)
if authoring != AUTHORING_VALUE:
errors.append(
f"{label}: unsupported {AUTHORING_ATTR} value {authoring!r}"
)
continue
if tag == "g":
errors.extend(
f"{label}: {error}"
for error in validate_authored_preset_group(element)
)
element_id = element.get("id")
if element_id and id_counts.get(element_id, 0) > 1:
errors.append(
f"{label}: authored preset logical group id must be "
"globally unique"
)
continue
if element.get("data-pptx-part") != "geometry":
errors.append(
f"{label}: authored preset metadata is allowed only on the "
"logical group and its direct geometry carrier"
)
continue
parent = parents.get(element)
if (
parent is None
or not _is_svg_element(parent, "g")
or parent.get(AUTHORING_ATTR) != AUTHORING_VALUE
):
errors.append(
f"{label}: authored preset geometry carrier must be a direct "
"child of its authored logical group"
)
return errors
def materialize_compact_authored_preset_tree(root: ET.Element) -> int:
"""Expand validated compact authored presets in memory for conversion.
Source SVG stays compact. The converter reuses the established lossless
carrier/preview path internally, so compact and expanded inputs share one
DrawingML implementation.
"""
materialized = 0
for group in list(root.iter()):
if authored_preset_encoding(group) != "compact":
continue
errors = _validate_compact_authored_preset_group(group)
if errors:
raise ValueError("; ".join(errors))
preset = group.get("data-pptx-prst") or ""
object_kind = group.get("data-pptx-object") or ""
frame = _parse_frame(group.get("data-pptx-frame"), object_kind)
adjustments = {
name[len(_ADJUSTMENT_PREFIX):]: value
for name, value in group.attrib.items()
if name.startswith(_ADJUSTMENT_PREFIX)
}
rendered = render_preset_geometry(
preset,
Xfrm(x=frame[0], y=frame[1], w=frame[2], h=frame[3]),
adjustments,
)
style_attrs = _validate_style({
name: group.attrib[name]
for name in _STYLE_ATTRS
if name in group.attrib
})
semantic_attrs = {
name: value
for name, value in group.attrib.items()
if name in _SEMANTIC_ATTRS
or name.startswith(_ADJUSTMENT_PREFIX)
or name == "data-pptx-shape-name"
}
markup = serialize_preset_layers(
rendered.paths,
semantic_attrs,
style_attrs,
)
for name in _STYLE_ATTRS:
group.attrib.pop(name, None)
group.set("data-pptx-preview-sha256", markup.preview_hash)
for child in list(group):
group.remove(child)
wrapper = ET.fromstring(
'<svg xmlns="http://www.w3.org/2000/svg">'
f"{markup.markup}"
"</svg>"
)
for child in list(wrapper):
wrapper.remove(child)
group.append(child)
materialized += 1
return materialized
def _validate_frame(
frame: tuple[float, float, float, float],
object_kind: str,
) -> tuple[float, float, float, float]:
if len(frame) != 4:
raise ValueError("frame must contain x, y, width, and height")
values = tuple(float(value) for value in frame)
if not all(math.isfinite(value) for value in values):
raise ValueError("frame values must be finite")
width, height = values[2], values[3]
if object_kind == "connector":
if width < 0 or height < 0 or (width == 0 and height == 0):
raise ValueError(
"connector frame dimensions must be non-negative and not both zero"
)
elif width <= 0 or height <= 0:
raise ValueError("shape frame width and height must be positive")
validate_ooxml_xfrm(
round(values[0] * EMU_PER_PX),
round(values[1] * EMU_PER_PX),
round(width * EMU_PER_PX),
round(height * EMU_PER_PX),
)
return values
def _parse_frame(
raw: str | None,
object_kind: str,
) -> tuple[float, float, float, float]:
if raw is None:
raise ValueError("authored preset requires data-pptx-frame")
parts = re.split(r"[\s,]+", raw.strip())
if len(parts) != 4:
raise ValueError("data-pptx-frame must contain four numbers")
return _validate_frame(tuple(float(part) for part in parts), object_kind)
def _validate_style(style: Mapping[str, str]) -> dict[str, str]:
unknown = sorted(set(style) - set(_STYLE_ATTRS))
if unknown:
raise ValueError(f"Unsupported authored preset style attributes: {unknown}")
normalized = {name: str(value).strip() for name, value in style.items()}
if not normalized:
raise ValueError("Authored preset requires explicit fill and/or stroke")
if normalized.get("fill", "none") == "none" and normalized.get(
"stroke", "none"
) == "none":
raise ValueError("Authored preset cannot have both fill and stroke set to none")
for name in ("fill", "stroke"):
value = normalized.get(name, "none")
if _PAINT_RE.fullmatch(value) is None:
raise ValueError(f"{name} must be none or a six-digit HEX color")
normalized[name] = value.upper() if value != "none" else value
if normalized.get("stroke", "none") == "none":
unused_stroke_attrs = sorted(
name for name in normalized
if name.startswith("stroke-")
)
if unused_stroke_attrs:
raise ValueError(
"Stroke presentation attributes require a visible stroke: "
+ ", ".join(unused_stroke_attrs)
)
if normalized.get("stroke-linecap") not in {None, "butt", "round", "square"}:
raise ValueError("stroke-linecap must be butt, round, or square")
if normalized.get("stroke-linejoin") not in {None, "miter", "round", "bevel"}:
raise ValueError("stroke-linejoin must be miter, round, or bevel")
for name in ("fill-opacity", "stroke-opacity"):
if name not in normalized:
continue
value = float(normalized[name])
if not math.isfinite(value) or value < 0 or value > 1:
raise ValueError(f"{name} must be between 0 and 1")
normalized[name] = fmt_num(value, 6)
if "stroke-width" in normalized:
width = float(normalized["stroke-width"])
if not math.isfinite(width) or width < 0:
raise ValueError("stroke-width must be finite and non-negative")
validate_ooxml_line_width(round(width * EMU_PER_PX))
normalized["stroke-width"] = fmt_num(width, 6)
if normalized.get("fill", "none") == "none" and "fill-opacity" in normalized:
raise ValueError("fill-opacity requires a visible fill paint")
if not _has_visible_fill(normalized) and not _has_visible_stroke(normalized):
raise ValueError(
"Authored preset requires at least one non-transparent visible paint"
)
return normalized
def _normalize_adjustments(
adjustments: Mapping[str, str | int | float],
) -> dict[str, str]:
normalized: dict[str, str] = {}
for name, value in adjustments.items():
if isinstance(value, bool):
raise ValueError(f"Adjustment {name!r} must not be boolean")
if isinstance(value, int):
formula = f"val {value}"
elif isinstance(value, float):
if not math.isfinite(value) or not value.is_integer():
raise ValueError(
f"Numeric adjustment {name!r} must be a finite integer"
)
formula = f"val {int(value)}"
else:
formula = str(value).strip()
if len(formula.split()) == 1:
formula = f"val {formula}"
normalized[str(name)] = formula
return normalized
def _validate_adjustments(
preset: str,
adjustments: Mapping[str, str | int | float],
) -> None:
registry = get_preset_registry()
if preset not in registry:
raise ValueError(f"Unknown DrawingML preset shape: {preset!r}")
for name, formula in adjustments.items():
if not isinstance(formula, str) or not formula.strip():
raise ValueError(f"Adjustment {name!r} requires a formula")
parts = formula.split()
if parts[0] not in SUPPORTED_OPERATORS:
raise ValueError(
f"Adjustment {name!r} must use a DrawingML formula operator"
)
if parts[0] == "val" and len(parts) == 2:
try:
float(parts[1])
except ValueError:
pass
else:
if _INTEGER_RE.fullmatch(parts[1]) is None:
raise ValueError(
f"Adjustment {name!r} val operand must be an integer "
"coordinate"
)
if not adjustments:
return
evaluated = registry.evaluate(
preset,
100000,
100000,
adjustments=adjustments,
)
for name, value in evaluated.adjustments.items():
if name not in adjustments:
continue
if not OOXML_COORDINATE_MIN <= value <= OOXML_COORDINATE_MAX:
raise ValueError(
f"Adjustment {name!r} evaluates outside OOXML coordinate range"
)
def _has_visible_fill(style: Mapping[str, str]) -> bool:
return (
style.get("fill", "none") != "none"
and float(style.get("fill-opacity", "1")) > 0
)
def _has_visible_stroke(style: Mapping[str, str]) -> bool:
return (
style.get("stroke", "none") != "none"
and float(style.get("stroke-opacity", "1")) > 0
and float(style.get("stroke-width", "1")) > 0
)
def _is_unexpected_carrier_attr(name: str) -> bool:
if name in {
"d",
"data-pptx-preview-sha256",
"data-pptx-part",
"data-pptx-shape-name",
"visibility",
"pointer-events",
*_SEMANTIC_ATTRS,
*_STYLE_ATTRS,
}:
return False
return not name.startswith(_ADJUSTMENT_PREFIX)
def _is_unexpected_group_attr(name: str, *, compact: bool) -> bool:
allowed = {
"id",
"transform",
"data-pptx-preview-sha256",
"data-pptx-shape-name",
*_SEMANTIC_ATTRS,
}
if compact:
allowed.update(_STYLE_ATTRS)
allowed.update(_TEMPLATE_ATOM_ATTRS)
if name in allowed:
return False
if name.startswith(_ADJUSTMENT_PREFIX):
return False
if name.startswith("data-pptx-runtime-") or name.startswith("aria-"):
return False
return name not in {"role", "tabindex"}
def _carrier_path(paths) -> str:
return " ".join(path.d for path in paths).strip()
def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def _is_svg_element(element: ET.Element, local_name: str) -> bool:
return element.tag in {
local_name,
f"{{{_SVG_NAMESPACE}}}{local_name}",
}
def _element_label(element: ET.Element) -> str:
tag = _local_name(element.tag)
element_id = element.get("id")
if element_id:
return f'<{tag} id="{element_id}">'
return f"<{tag}>"