103 lines
4.1 KiB
Python
103 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Swap every label in a .drawio via a mapping — layout, styles, ids untouched.
|
|
|
|
The main use-case is language variants of one diagram (EN <-> CN docs figures):
|
|
extract the labels, translate the values, apply the map — the geometry never
|
|
moves, so both variants stay pixel-identical except for the text.
|
|
|
|
python3 relabel.py diagram.drawio --extract -o labels.json # step 1
|
|
# edit labels.json: keep keys, replace each value with the new text
|
|
python3 relabel.py diagram.drawio --map labels.json -o diagram_cn.drawio
|
|
|
|
Extract emits an identity JSON map {"label": "label", ...} of every non-empty
|
|
vertex/edge label, UserObject label, and page name, in document order. Apply
|
|
replaces each label that exactly matches a map key (raw string, HTML markup
|
|
included) and reports what matched. Unmapped labels stay unchanged; map keys
|
|
that matched nothing are listed on stderr so translations don't silently miss.
|
|
|
|
Usage: relabel.py <file.drawio> (--extract | --map <labels.json>) [-o <out>]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
def label_slots(tree):
|
|
"""Yield (element, attribute) for every label-bearing slot in the file."""
|
|
for diagram in tree.getroot().iter("diagram"):
|
|
if diagram.get("name"):
|
|
yield diagram, "name"
|
|
model = diagram.find("mxGraphModel")
|
|
root = model.find("root") if model is not None else None
|
|
if root is None: # compressed page — can't edit
|
|
sys.stderr.write("warning: skipping compressed page "
|
|
f"'{diagram.get('name', '?')}' (open+save in draw.io to decompress)\n")
|
|
continue
|
|
for child in root:
|
|
if child.tag == "mxCell":
|
|
if child.get("value"):
|
|
yield child, "value"
|
|
elif child.tag in ("UserObject", "object"):
|
|
if child.get("label"):
|
|
yield child, "label"
|
|
inner = child.find("mxCell")
|
|
if inner is not None and inner.get("value"):
|
|
yield inner, "value"
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Extract or swap .drawio labels via a JSON map.")
|
|
p.add_argument("file", help="input .drawio")
|
|
mode = p.add_mutually_exclusive_group(required=True)
|
|
mode.add_argument("--extract", action="store_true",
|
|
help="dump an identity label map as JSON")
|
|
mode.add_argument("--map", metavar="JSON", dest="mapfile",
|
|
help="JSON map {old label: new label} to apply")
|
|
p.add_argument("-o", "--output", help="output path (default: stdout for --extract, "
|
|
"<file>-relabel.drawio for --map)")
|
|
args = p.parse_args()
|
|
|
|
if not os.path.isfile(args.file):
|
|
sys.exit(f"error: {args.file} not found")
|
|
tree = ET.parse(args.file)
|
|
|
|
if args.extract:
|
|
seen = {}
|
|
for el, attr in label_slots(tree):
|
|
seen.setdefault(el.get(attr), el.get(attr))
|
|
out = json.dumps(seen, ensure_ascii=False, indent=2)
|
|
if args.output:
|
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
f.write(out + "\n")
|
|
sys.stderr.write(f"wrote {args.output} ({len(seen)} labels)\n")
|
|
else:
|
|
print(out)
|
|
return
|
|
|
|
with open(args.mapfile, encoding="utf-8") as f:
|
|
mapping = json.load(f)
|
|
if not isinstance(mapping, dict):
|
|
sys.exit("error: map file must be a JSON object {old: new}")
|
|
|
|
matched, used = 0, set()
|
|
for el, attr in label_slots(tree):
|
|
old = el.get(attr)
|
|
if old in mapping:
|
|
el.set(attr, str(mapping[old]))
|
|
matched += 1
|
|
used.add(old)
|
|
|
|
out = args.output or os.path.splitext(args.file)[0] + "-relabel.drawio"
|
|
tree.write(out, encoding="utf-8", xml_declaration=False)
|
|
unused = [k for k in mapping if k not in used]
|
|
if unused:
|
|
sys.stderr.write("warning: %d map key(s) matched no label: %s\n"
|
|
% (len(unused), ", ".join(repr(k)[:60] for k in unused[:10])))
|
|
sys.stderr.write(f"wrote {out} ({matched} labels replaced)\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|