578 lines
21 KiB
Python
578 lines
21 KiB
Python
"""In-memory expansion of project icons and static local ``<use>`` elements.
|
|
|
|
The icon placeholder ``<use data-icon="...">`` is a project-internal SVG
|
|
extension; standard renderers (browsers, PowerPoint's SVG parser) and our
|
|
own DrawingML dispatcher do not understand it. ``finalize_svg`` already
|
|
expands it on disk into ``svg_final/``; this module provides the same
|
|
expansion in memory so ``svg_to_pptx`` can consume ``svg_output/`` directly
|
|
without first running the on-disk finalize step.
|
|
|
|
Public API:
|
|
expand_use_data_icons(root, icons_dir, fallback_dir=None) -> int
|
|
Walk the SVG element tree, replace every ``<use data-icon="...">``
|
|
with its expanded ``<g>`` group of primitive shapes, and return
|
|
the number of replacements made.
|
|
expand_local_use_references(root) -> int
|
|
Materialize same-document ``href="#id"`` references, including
|
|
``<symbol>`` viewBox mapping, and return the number of instances.
|
|
expand_local_use_references_in_file(svg_path) -> int
|
|
Apply the same expansion to an SVG file in place.
|
|
|
|
The heavy lifting (icon resolution, color application, scaling) is
|
|
delegated to ``svg_finalize.embed_icons`` so the two pipelines stay
|
|
behaviourally aligned.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import math
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from xml.etree import ElementTree as ET
|
|
|
|
|
|
SVG_NS = 'http://www.w3.org/2000/svg'
|
|
XLINK_NS = 'http://www.w3.org/1999/xlink'
|
|
|
|
_LOCAL_HREF_RE = re.compile(r'^#([^#\s]+)$')
|
|
_LENGTH_RE = re.compile(
|
|
r'^\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)\s*(?:px)?\s*$'
|
|
)
|
|
_VIEWBOX_SPLIT_RE = re.compile(r'[\s,]+')
|
|
_URL_REF_RE = re.compile(r'''url\(#([^#\s()'"\\]+)\)''')
|
|
_URL_FUNCTION_RE = re.compile(r'\burl\s*\([^)]*\)', re.IGNORECASE)
|
|
_URL_FUNCTION_START_RE = re.compile(r'\burl\s*\(', re.IGNORECASE)
|
|
_MAX_LOCAL_USE_DEPTH = 64
|
|
_MAX_LOCAL_USE_INSTANCES = 10_000
|
|
_NON_REUSABLE_METADATA_PREFIXES = (
|
|
'data-pptx-authoring',
|
|
'data-pptx-object',
|
|
'data-pptx-prst',
|
|
'data-pptx-frame',
|
|
'data-pptx-av-',
|
|
'data-pptx-layer',
|
|
'data-pptx-replace-with',
|
|
'data-pptx-replacement-',
|
|
'data-pptx-import-source',
|
|
'data-pptx-fallback-',
|
|
'data-pptx-native',
|
|
'data-pptx-visual-status',
|
|
'data-pptx-route-status',
|
|
'data-pptx-placeholder',
|
|
)
|
|
_REFERENCE_TAGS = frozenset({
|
|
'symbol', 'g', 'use',
|
|
'rect', 'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline',
|
|
'text', 'image',
|
|
})
|
|
|
|
|
|
class UseExpansionError(ValueError):
|
|
"""Raised when a static local ``<use>`` cannot be expanded safely."""
|
|
|
|
|
|
def _local_tag(elem: ET.Element) -> str:
|
|
"""Return an ElementTree node's local tag name."""
|
|
return elem.tag.rsplit('}', 1)[-1] if '}' in str(elem.tag) else str(elem.tag)
|
|
|
|
|
|
def _qualified_tag(elem: ET.Element, local: str) -> str:
|
|
"""Return ``local`` in the same namespace as ``elem``."""
|
|
tag = str(elem.tag)
|
|
if tag.startswith('{') and '}' in tag:
|
|
return f'{tag.split("}", 1)[0]}}}{local}'
|
|
return local
|
|
|
|
|
|
def _fmt_number(value: float) -> str:
|
|
"""Format a finite SVG transform number without negative zero."""
|
|
if abs(value) < 1e-12:
|
|
value = 0.0
|
|
return f'{value:.12g}'
|
|
|
|
|
|
def _numeric_length(
|
|
elem: ET.Element,
|
|
name: str,
|
|
default: float | None = None,
|
|
) -> float | None:
|
|
"""Parse a unitless/px local-use geometry value."""
|
|
raw = elem.get(name)
|
|
if raw is None:
|
|
return default
|
|
match = _LENGTH_RE.fullmatch(raw)
|
|
if match is None:
|
|
raise UseExpansionError(
|
|
f'<use> {name} must be a finite unitless or px value, got {raw!r}'
|
|
)
|
|
value = float(match.group(1))
|
|
if not math.isfinite(value):
|
|
raise UseExpansionError(f'<use> {name} must be finite, got {raw!r}')
|
|
return value
|
|
|
|
|
|
def _parse_viewbox(symbol: ET.Element) -> tuple[float, float, float, float]:
|
|
"""Parse a positive four-number symbol viewBox."""
|
|
raw = symbol.get('viewBox', '')
|
|
parts = [part for part in _VIEWBOX_SPLIT_RE.split(raw.strip()) if part]
|
|
if len(parts) != 4:
|
|
raise UseExpansionError(
|
|
'<symbol> referenced by <use> must define a four-number viewBox'
|
|
)
|
|
try:
|
|
values = tuple(float(part) for part in parts)
|
|
except ValueError as exc:
|
|
raise UseExpansionError(
|
|
f'<symbol> viewBox contains a non-numeric value: {raw!r}'
|
|
) from exc
|
|
if not all(math.isfinite(value) for value in values):
|
|
raise UseExpansionError(f'<symbol> viewBox must be finite, got {raw!r}')
|
|
min_x, min_y, width, height = values
|
|
if width <= 0 or height <= 0:
|
|
raise UseExpansionError(
|
|
f'<symbol> viewBox width/height must be positive, got {raw!r}'
|
|
)
|
|
return min_x, min_y, width, height
|
|
|
|
|
|
def _symbol_viewport_transform(symbol: ET.Element, use_elem: ET.Element) -> str:
|
|
"""Map one symbol viewBox into the use element's explicit viewport."""
|
|
for attr in ('refX', 'refY'):
|
|
if symbol.get(attr) is not None:
|
|
raise UseExpansionError(
|
|
f'<symbol> {attr} is not supported by local <use> expansion'
|
|
)
|
|
|
|
min_x, min_y, view_width, view_height = _parse_viewbox(symbol)
|
|
width = _numeric_length(use_elem, 'width')
|
|
height = _numeric_length(use_elem, 'height')
|
|
if width is None or height is None or width <= 0 or height <= 0:
|
|
raise UseExpansionError(
|
|
'<use> referencing <symbol> requires positive numeric width and height'
|
|
)
|
|
|
|
raw_aspect = symbol.get('preserveAspectRatio', 'xMidYMid meet').strip()
|
|
parts = raw_aspect.split()
|
|
if parts and parts[0] == 'defer':
|
|
parts.pop(0)
|
|
align = parts[0] if parts else 'xMidYMid'
|
|
mode = parts[1] if len(parts) > 1 else 'meet'
|
|
if len(parts) > 2 or mode not in {'meet', 'slice'}:
|
|
raise UseExpansionError(
|
|
f'Unsupported symbol preserveAspectRatio: {raw_aspect!r}'
|
|
)
|
|
|
|
if align == 'none':
|
|
if len(parts) > 1:
|
|
raise UseExpansionError(
|
|
f'Unsupported symbol preserveAspectRatio: {raw_aspect!r}'
|
|
)
|
|
scale_x = width / view_width
|
|
scale_y = height / view_height
|
|
translate_x = -min_x * scale_x
|
|
translate_y = -min_y * scale_y
|
|
else:
|
|
alignments = {
|
|
'xMinYMin': (0.0, 0.0), 'xMidYMin': (0.5, 0.0), 'xMaxYMin': (1.0, 0.0),
|
|
'xMinYMid': (0.0, 0.5), 'xMidYMid': (0.5, 0.5), 'xMaxYMid': (1.0, 0.5),
|
|
'xMinYMax': (0.0, 1.0), 'xMidYMax': (0.5, 1.0), 'xMaxYMax': (1.0, 1.0),
|
|
}
|
|
if align not in alignments:
|
|
raise UseExpansionError(
|
|
f'Unsupported symbol preserveAspectRatio: {raw_aspect!r}'
|
|
)
|
|
if mode == 'slice':
|
|
raise UseExpansionError(
|
|
'symbol preserveAspectRatio="... slice" requires viewport clipping, '
|
|
'which local <use> expansion does not approximate'
|
|
)
|
|
scale = min(width / view_width, height / view_height)
|
|
scale_x = scale_y = scale
|
|
align_x, align_y = alignments[align]
|
|
translate_x = (width - view_width * scale) * align_x - min_x * scale
|
|
translate_y = (height - view_height * scale) * align_y - min_y * scale
|
|
|
|
return (
|
|
f'matrix({_fmt_number(scale_x)} 0 0 {_fmt_number(scale_y)} '
|
|
f'{_fmt_number(translate_x)} {_fmt_number(translate_y)})'
|
|
)
|
|
|
|
|
|
class _LocalUseExpander:
|
|
"""Materialize static same-document SVG use references."""
|
|
|
|
def __init__(self, root: ET.Element):
|
|
self.root = root
|
|
self.targets: dict[str, ET.Element] = {}
|
|
self.duplicate_ids: set[str] = set()
|
|
self.used_ids: set[str] = set()
|
|
self.instance_index = 0
|
|
self.instances_started = 0
|
|
self.expanded = 0
|
|
for elem in root.iter():
|
|
elem_id = elem.get('id')
|
|
if not elem_id:
|
|
continue
|
|
self.used_ids.add(elem_id)
|
|
if elem_id in self.targets:
|
|
self.duplicate_ids.add(elem_id)
|
|
else:
|
|
self.targets[elem_id] = elem
|
|
|
|
def expand(self) -> int:
|
|
"""Expand every visible non-data-icon use node in the document."""
|
|
self._expand_children(self.root, ())
|
|
return self.expanded
|
|
|
|
def _expand_children(self, parent: ET.Element, stack: tuple[str, ...]) -> None:
|
|
for index, child in enumerate(list(parent)):
|
|
if _local_tag(child) == 'defs':
|
|
continue
|
|
if _local_tag(child) == 'use' and not child.get('data-icon'):
|
|
replacement = self._materialize_use(child, stack)
|
|
parent.remove(child)
|
|
parent.insert(index, replacement)
|
|
continue
|
|
self._expand_children(child, stack)
|
|
|
|
def _materialize_use(
|
|
self,
|
|
use_elem: ET.Element,
|
|
stack: tuple[str, ...],
|
|
) -> ET.Element:
|
|
self.instances_started += 1
|
|
if self.instances_started > _MAX_LOCAL_USE_INSTANCES:
|
|
raise UseExpansionError(
|
|
'Local <use> expansion exceeds the 10000-instance safety limit'
|
|
)
|
|
|
|
href = use_elem.get('href')
|
|
xlink_href = use_elem.get(f'{{{XLINK_NS}}}href')
|
|
if href is not None and xlink_href is not None:
|
|
if href != xlink_href:
|
|
raise UseExpansionError(
|
|
'Conflicting href and xlink:href values on local <use>'
|
|
)
|
|
if href is None:
|
|
href = xlink_href
|
|
match = _LOCAL_HREF_RE.fullmatch(href or '')
|
|
if match is None:
|
|
raise UseExpansionError(
|
|
'<use> must reference a same-document fragment with href="#id"; '
|
|
f'got {href!r}'
|
|
)
|
|
ref_id = match.group(1)
|
|
if len(stack) >= _MAX_LOCAL_USE_DEPTH:
|
|
chain = ' -> '.join((*stack, ref_id))
|
|
raise UseExpansionError(
|
|
f'Local <use> expansion exceeds the 64-reference depth limit: {chain}'
|
|
)
|
|
if ref_id in self.duplicate_ids:
|
|
raise UseExpansionError(
|
|
f'<use href="#{ref_id}"> is ambiguous because the id is duplicated'
|
|
)
|
|
use_id = use_elem.get('id')
|
|
if use_id and use_id in self.duplicate_ids:
|
|
raise UseExpansionError(
|
|
f'Local <use> instance id {use_id!r} is duplicated in this SVG'
|
|
)
|
|
target = self.targets.get(ref_id)
|
|
if target is None:
|
|
raise UseExpansionError(
|
|
f'<use href="#{ref_id}"> has no matching element in this SVG'
|
|
)
|
|
if ref_id in stack:
|
|
chain = ' -> '.join((*stack, ref_id))
|
|
raise UseExpansionError(f'Circular local <use> reference: {chain}')
|
|
|
|
target_tag = _local_tag(target)
|
|
if target_tag not in _REFERENCE_TAGS:
|
|
raise UseExpansionError(
|
|
f'<use href="#{ref_id}"> references unsupported <{target_tag}>'
|
|
)
|
|
|
|
self._reject_structural_metadata(use_elem, 'instance')
|
|
self._reject_structural_metadata(target, f'target #{ref_id}')
|
|
self._validate_fragment_reference_syntax(use_elem, 'instance')
|
|
self._validate_fragment_reference_syntax(target, f'target #{ref_id}')
|
|
|
|
target_ids = {
|
|
elem_id
|
|
for elem in target.iter()
|
|
if (elem_id := elem.get('id'))
|
|
}
|
|
ambiguous_ids = sorted(target_ids & self.duplicate_ids)
|
|
if ambiguous_ids:
|
|
joined = ', '.join(ambiguous_ids)
|
|
raise UseExpansionError(
|
|
f'<use href="#{ref_id}"> references a subtree with duplicate id(s): '
|
|
f'{joined}'
|
|
)
|
|
|
|
next_stack = (*stack, ref_id)
|
|
target_clone = copy.deepcopy(target)
|
|
if target_tag == 'use':
|
|
clone = self._materialize_use(target_clone, next_stack)
|
|
else:
|
|
clone = target_clone
|
|
if target_tag == 'symbol':
|
|
clone.tag = _qualified_tag(clone, 'g')
|
|
viewport_transform = _symbol_viewport_transform(target, use_elem)
|
|
existing_transform = clone.get('transform', '').strip()
|
|
clone.set(
|
|
'transform',
|
|
f'{existing_transform} {viewport_transform}'.strip(),
|
|
)
|
|
for attr in ('viewBox', 'preserveAspectRatio', 'x', 'y', 'width', 'height'):
|
|
clone.attrib.pop(attr, None)
|
|
self._expand_children(clone, next_stack)
|
|
|
|
self._rewrite_clone_ids(clone, self._next_instance_prefix(clone))
|
|
wrapper = self._build_wrapper(use_elem)
|
|
wrapper.append(clone)
|
|
self.expanded += 1
|
|
return wrapper
|
|
|
|
@staticmethod
|
|
def _reject_structural_metadata(elem: ET.Element, label: str) -> None:
|
|
"""Reject reusable template/native markers parsed before expansion."""
|
|
for candidate in elem.iter():
|
|
for attr in candidate.attrib:
|
|
if attr.startswith(_NON_REUSABLE_METADATA_PREFIXES):
|
|
raise UseExpansionError(
|
|
f'Local <use> {label} cannot carry structural {attr} metadata'
|
|
)
|
|
|
|
def _validate_fragment_reference_syntax(
|
|
self,
|
|
elem: ET.Element,
|
|
label: str,
|
|
) -> None:
|
|
"""Reject URL fragment forms the clone rewriter cannot preserve."""
|
|
for candidate in elem.iter():
|
|
for value in candidate.attrib.values():
|
|
starts = list(_URL_FUNCTION_START_RE.finditer(value))
|
|
if not starts:
|
|
continue
|
|
functions = list(_URL_FUNCTION_RE.finditer(value))
|
|
if len(functions) != len(starts):
|
|
raise UseExpansionError(
|
|
f'Local <use> {label} has malformed url(...) reference {value!r}'
|
|
)
|
|
for function in functions:
|
|
raw = function.group(0)
|
|
match = _URL_REF_RE.fullmatch(raw)
|
|
if match is None:
|
|
raise UseExpansionError(
|
|
f'Local <use> {label} requires exact url(#id) fragments; '
|
|
f'got {raw!r}'
|
|
)
|
|
ref_id = match.group(1)
|
|
if ref_id in self.duplicate_ids:
|
|
raise UseExpansionError(
|
|
f'Local <use> {label} has ambiguous url(#{ref_id}); '
|
|
'the referenced id is duplicated'
|
|
)
|
|
if ref_id not in self.targets:
|
|
raise UseExpansionError(
|
|
f'Local <use> {label} has unresolved url(#{ref_id})'
|
|
)
|
|
|
|
def _next_instance_prefix(self, clone: ET.Element) -> str:
|
|
"""Reserve a deterministic clone prefix that cannot collide."""
|
|
clone_ids = {
|
|
elem_id
|
|
for elem in clone.iter()
|
|
if (elem_id := elem.get('id'))
|
|
}
|
|
while True:
|
|
self.instance_index += 1
|
|
prefix = f'use-instance-{self.instance_index}-'
|
|
generated_ids = {f'{prefix}{elem_id}' for elem_id in clone_ids}
|
|
if not generated_ids & self.used_ids:
|
|
self.used_ids.update(generated_ids)
|
|
return prefix
|
|
|
|
@staticmethod
|
|
def _rewrite_clone_ids(clone: ET.Element, prefix: str) -> None:
|
|
"""Make materialized IDs instance-local and rewrite fragment refs."""
|
|
id_map: dict[str, str] = {}
|
|
for elem in clone.iter():
|
|
elem_id = elem.get('id')
|
|
if elem_id:
|
|
id_map[elem_id] = f'{prefix}{elem_id}'
|
|
if not id_map:
|
|
return
|
|
for elem in clone.iter():
|
|
elem_id = elem.get('id')
|
|
if elem_id in id_map:
|
|
elem.set('id', id_map[elem_id])
|
|
for attr, value in list(elem.attrib.items()):
|
|
if attr in {'href', f'{{{XLINK_NS}}}href'} and value.startswith('#'):
|
|
ref_id = value[1:]
|
|
if ref_id in id_map:
|
|
elem.set(attr, f'#{id_map[ref_id]}')
|
|
continue
|
|
rewritten = _URL_REF_RE.sub(
|
|
lambda match: f'url(#{id_map.get(match.group(1), match.group(1))})',
|
|
value,
|
|
)
|
|
if rewritten != value:
|
|
elem.set(attr, rewritten)
|
|
|
|
@staticmethod
|
|
def _build_wrapper(use_elem: ET.Element) -> ET.Element:
|
|
"""Create an inheriting group for use styles and instance geometry."""
|
|
wrapper = ET.Element(_qualified_tag(use_elem, 'g'))
|
|
skipped = {
|
|
'href', f'{{{XLINK_NS}}}href',
|
|
'x', 'y', 'width', 'height', 'preserveAspectRatio', 'transform',
|
|
}
|
|
for attr, value in use_elem.attrib.items():
|
|
if attr not in skipped:
|
|
wrapper.set(attr, value)
|
|
|
|
x = _numeric_length(use_elem, 'x', 0.0) or 0.0
|
|
y = _numeric_length(use_elem, 'y', 0.0) or 0.0
|
|
transforms = []
|
|
use_transform = use_elem.get('transform', '').strip()
|
|
if use_transform:
|
|
transforms.append(use_transform)
|
|
if x or y:
|
|
transforms.append(f'translate({_fmt_number(x)} {_fmt_number(y)})')
|
|
if transforms:
|
|
wrapper.set('transform', ' '.join(transforms))
|
|
return wrapper
|
|
|
|
|
|
def expand_local_use_references(root: ET.Element) -> int:
|
|
"""Expand static same-document ``<use href="#id">`` references."""
|
|
return _LocalUseExpander(root).expand()
|
|
|
|
|
|
def validate_local_use_references(root: ET.Element) -> list[str]:
|
|
"""Return expansion errors without mutating the caller's SVG tree."""
|
|
try:
|
|
expand_local_use_references(copy.deepcopy(root))
|
|
except UseExpansionError as exc:
|
|
return [str(exc)]
|
|
return []
|
|
|
|
|
|
def expand_local_use_references_in_file(svg_path: Path) -> int:
|
|
"""Expand local use references in one SVG file in place."""
|
|
tree = ET.parse(str(svg_path))
|
|
count = expand_local_use_references(tree.getroot())
|
|
if count:
|
|
ET.register_namespace('', SVG_NS)
|
|
ET.register_namespace('xlink', XLINK_NS)
|
|
tree.write(str(svg_path), encoding='unicode', xml_declaration=False)
|
|
return count
|
|
|
|
|
|
def _import_embed_icons():
|
|
"""Lazy import so svg_to_pptx doesn't hard-require svg_finalize at import time."""
|
|
scripts_dir = Path(__file__).resolve().parent.parent
|
|
if str(scripts_dir) not in sys.path:
|
|
sys.path.insert(0, str(scripts_dir))
|
|
from svg_finalize import embed_icons # type: ignore
|
|
return embed_icons
|
|
|
|
|
|
def _build_replacement_g(
|
|
use_elem: ET.Element,
|
|
icons_dir: Path,
|
|
fallback_dir: Path | None,
|
|
embed_icons_mod,
|
|
) -> ET.Element | None:
|
|
"""Resolve a single ``<use data-icon="...">`` into an expanded ``<g>``.
|
|
|
|
Returns None when the icon name is missing, unresolved, or the icon
|
|
file cannot be parsed. Callers should leave the original ``<use>`` in
|
|
place in that case (matching the on-disk finalize_svg behaviour, which
|
|
also leaves unresolvable placeholders untouched).
|
|
"""
|
|
use_str = ET.tostring(use_elem, encoding='unicode')
|
|
attrs = embed_icons_mod.parse_use_element(use_str)
|
|
if 'icon' not in attrs:
|
|
return None
|
|
|
|
icon_path, _base_size = embed_icons_mod.resolve_icon_path(
|
|
attrs['icon'], icons_dir, fallback_dir,
|
|
)
|
|
if not icon_path.exists():
|
|
return None
|
|
|
|
color = attrs.get('fill', '#000000')
|
|
elements, style, base_size = embed_icons_mod.extract_paths_from_icon(
|
|
icon_path, color,
|
|
)
|
|
if not elements:
|
|
return None
|
|
|
|
g_xml = embed_icons_mod.generate_icon_group(attrs, elements, style, base_size)
|
|
|
|
# Wrap with a namespaced root so the parsed subtree carries the SVG
|
|
# namespace through to every primitive (path/circle/...).
|
|
wrapped = f'<svg xmlns="{SVG_NS}">{g_xml}</svg>'
|
|
try:
|
|
parsed_root = ET.fromstring(wrapped)
|
|
except ET.ParseError:
|
|
return None
|
|
|
|
for child in parsed_root:
|
|
local = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
|
if local == 'g':
|
|
return child
|
|
return None
|
|
|
|
|
|
def expand_use_data_icons(
|
|
root: ET.Element,
|
|
icons_dir: Path,
|
|
fallback_dir: Path | None = None,
|
|
) -> int:
|
|
"""Replace every ``<use data-icon="...">`` in *root* with its expansion.
|
|
|
|
Walks the tree, finds use elements that carry a ``data-icon`` attribute,
|
|
builds a new ``<g>`` subtree from the project icon library (falling back to
|
|
the global library when supplied), and swaps it into the parent element at
|
|
the same position.
|
|
|
|
Returns the number of placeholders successfully expanded. Unresolvable
|
|
placeholders are left in place so callers can decide whether to warn.
|
|
"""
|
|
if not icons_dir.exists():
|
|
return 0
|
|
|
|
embed_icons_mod = _import_embed_icons()
|
|
|
|
# ElementTree elements don't carry a parent reference, so build a map.
|
|
parent_of: dict[ET.Element, ET.Element] = {}
|
|
for parent in root.iter():
|
|
for child in parent:
|
|
parent_of[child] = parent
|
|
|
|
targets: list[ET.Element] = []
|
|
for elem in root.iter():
|
|
local = elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag
|
|
if local == 'use' and elem.get('data-icon'):
|
|
targets.append(elem)
|
|
|
|
expanded = 0
|
|
for use_elem in targets:
|
|
parent = parent_of.get(use_elem)
|
|
if parent is None:
|
|
continue
|
|
replacement = _build_replacement_g(use_elem, icons_dir, fallback_dir, embed_icons_mod)
|
|
if replacement is None:
|
|
continue
|
|
idx = list(parent).index(use_elem)
|
|
parent.remove(use_elem)
|
|
parent.insert(idx, replacement)
|
|
expanded += 1
|
|
|
|
return expanded
|