"""Конвертирует нативный .archimate (формат Archi) в The Open Group ArchiMate Model Exchange File Format — его понимают сторонние инструменты. Вложенность узлов на схемах разворачивается в плоский список с абсолютными координатами: в обменном формате трактовка координат вложенных узлов неоднозначна, а плоская раскладка даёт одинаковый результат везде. Контейнер выводится раньше содержимого, поэтому визуально вложенность сохраняется. """ import sys, io, pathlib import xml.etree.ElementTree as ET from xml.sax.saxutils import escape, quoteattr sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") XSI = "{http://www.w3.org/2001/XMLSchema-instance}type" NS = "http://www.opengroup.org/xsd/archimate/3.0/" src, dst = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]) root = ET.parse(src).getroot() def t(e): return (e.get(XSI) or "").split(":")[-1] def text(e, tag): c = e.find(tag) return c.text if c is not None and c.text else "" elements, relationships, views = [], [], [] for folder in root.findall("folder"): ftype = folder.get("type") for e in folder.findall("element"): et = t(e) if ftype == "diagrams": views.append(e) elif et.endswith("Relationship"): relationships.append((e.get("id"), et[:-len("Relationship")], e.get("source"), e.get("target"))) else: elements.append((e.get("id"), et, e.get("name") or "", text(e, "documentation"))) def flatten(node, ox=0, oy=0, out=None, conns=None): """Возвращает плоский список узлов с абсолютными координатами и соединения.""" out = [] if out is None else out conns = [] if conns is None else conns b = node.find("bounds") x = ox + int(b.get("x", 0)); y = oy + int(b.get("y", 0)) # у надписей (Note) нет archimateElement — в обменном формате это узел типа Label out.append((node.get("id"), node.get("archimateElement"), x, y, int(b.get("width", 120)), int(b.get("height", 55)), text(node, "content"))) for c in node.findall("sourceConnection"): conns.append((c.get("id"), c.get("relationship"), c.get("source"), c.get("target"))) for ch in node.findall("child"): flatten(ch, x, y, out, conns) return out, conns out = ['', f'', f' {escape(root.get("name") or "model")}'] purpose = root.find("purpose") if purpose is not None and purpose.text: out.append(f' {escape(purpose.text)}') out.append(" ") for eid, etype, name, doc in elements: out.append(f' ') out.append(f' {escape(name)}') if doc: out.append(f' {escape(doc)}') out.append(" ") out.append(" ") out.append(" ") for rid, rtype, s, tg in relationships: out.append(f' ') out.append(" ") out.append(" ") out.append(" ") for v in views: out.append(f' ') out.append(f' {escape(v.get("name") or "")}') doc = text(v, "documentation") if doc: out.append(f' {escape(doc)}') nodes, conns = [], [] for child in v.findall("child"): flatten(child, 0, 0, nodes, conns) for nid, eref, x, y, w, h, content in nodes: if eref: out.append(f' ') else: out.append(f' ') out.append(f' ') out.append(' ') for cid, rref, s, tg in conns: out.append(f' ') out.append(" ") out.append(" ") out.append(" ") out.append("") dst.write_text("\n".join(out) + "\n", encoding="utf-8", newline="\n") print(f"записано: {dst}") print(f" элементов: {len(elements)}") print(f" отношений: {len(relationships)}") print(f" представлений: {len(views)}")